Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b9e6bad0b | |||
| 9a5ff7a619 | |||
| 5d40cc112d | |||
| aa19897031 | |||
| afd7f957df | |||
| cca4b4bb8e | |||
| b848a83777 | |||
| c0c40c0387 | |||
| 606421776b | |||
| 47009912df | |||
| fea8fcaf61 | |||
| e562f6a24f | |||
| 23acf203e9 | |||
| 94ae940e82 |
@@ -112,17 +112,16 @@ of a component, rendered by Owl. It only work on a html element tagged by the
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="someDiv"/>
|
||||
<input t-ref="someInput"/>
|
||||
<span>hello</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
`SubComponent` with the `useRef` hook:
|
||||
In this example, the component will be able to access the `input` with the `useRef` hook:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
inputRef = useRef("someComponent");
|
||||
inputRef = useRef("someInput");
|
||||
|
||||
someMethod() {
|
||||
// here, if component is mounted, refs are active:
|
||||
@@ -139,18 +138,18 @@ The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
`t-component` directives). For example,
|
||||
|
||||
```xml
|
||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
|
||||
```
|
||||
|
||||
Here, the references need to be set like this:
|
||||
|
||||
```js
|
||||
this.ref1 = useRef("component_1");
|
||||
this.ref2 = useRef("component_2");
|
||||
this.ref1 = useRef("div_1");
|
||||
this.ref2 = useRef("div_2");
|
||||
```
|
||||
|
||||
References are only guaranteed to be active while the parent component is mounted.
|
||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||
If this is not the case, accessing `el` on it will return `null`.
|
||||
|
||||
### `useSubEnv` and `useChildSubEnv`
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `values`: if the type was `Object`, then the `values` key describes the interface of values in the object, this allows validating objects that are used as mappings, where keys are not known in advance but the shape of the values is.
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
- `optional`: if true, the prop is not mandatory
|
||||
@@ -276,6 +277,10 @@ class ComponentB extends owl.Component {
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someObj3: {
|
||||
type: Object,
|
||||
values: { type: Array, element: String },
|
||||
}, // object with arbitary keys where values are arrays of strings
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date], // either a boolean or a date
|
||||
otherValue: true, // indicates that it is a prop
|
||||
|
||||
@@ -5,7 +5,9 @@ to the browser devtools in order to inspect all owl apps that are present on any
|
||||
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
|
||||
the components' lifecycle and be able to trace their origin.
|
||||
|
||||
## Install the extension
|
||||
See the [`devtools doc`](devtools_guide.md) for more information.
|
||||
|
||||
## Install the extension manually (for devs)
|
||||
|
||||
In the owl root folder:
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Owl Devtools Guide
|
||||
|
||||
## Information popup
|
||||
|
||||
After having installed the extension, a new icon will be added to your extension bar.
|
||||
If you don't see it, you can pin the extension using the extensions popup.
|
||||
|
||||
<img src="screenshots/extensions.png"/>
|
||||
|
||||
Clicking on the owl icon will open the information popup. This popup is useful
|
||||
to know in advance whether owl is loaded in the tab or not. This is also indicated
|
||||
by the icon itself: if it is flipped upside-down, it means that owl is not loaded in the
|
||||
active tab. Do note that old versions of owl are not supported by the extension and will
|
||||
therefore be indicated either as obsolete or absent by the extension popup.
|
||||
|
||||
<img src="screenshots/popup.png"/>
|
||||
|
||||
## First steps
|
||||
|
||||
When you are on a page where owl is detected, you can open your devtools either with
|
||||
right-click -> Inspect or using F12. In the devtools menu, you can search for the Owl
|
||||
tab which is added by the extension. It will be present by default at the end of the list but
|
||||
you can drag and drop it at the position you want for easier navigation in the future.
|
||||
|
||||
<img src="screenshots/find_owl_tab.png"/>
|
||||
|
||||
When you open the tab, you arrive on the Components view by default which is one of the
|
||||
two available tabs at the top. Here is an example of the devtools on the Odoo CRM app:
|
||||
|
||||
<img src="screenshots/crm.png"/>
|
||||
|
||||
## Components tab
|
||||
|
||||
The components tab is separated into two sub windows: the components tree in the left and
|
||||
the component details in the right. The components tree will display all the different
|
||||
components that are present in the tab in the form of a tree. The root of this tree is
|
||||
actually the app which is not a component but can still be inspected by the devtools like
|
||||
one. There can also be multiple apps loaded in the page like in the following:
|
||||
|
||||
<img src="screenshots/multi_apps.png"/>
|
||||
|
||||
There is a convenient search bar at the top of the components tree which will help finding
|
||||
the components tou want in the tree and also, an element picker can be used to directly select
|
||||
the component you want to focus on in the page which is especially useful when trying to find
|
||||
what you want. Just click on the elements picker icon and click on the element you want to focus
|
||||
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
||||
page in this mode will highlight it and the same happens anytime in the components tree.
|
||||
|
||||
<img src="screenshots/picker.png"/>
|
||||
|
||||
In the tree itself, the navigation is quite simple and is similar to the one in the Elements tab
|
||||
of the browser's devtools. It is possible to navigate with the keyboard using the arrow keys and
|
||||
multiple shortcuts are available in a custom menu when right-clicking on a component. This menu
|
||||
allows to expand/fold all the children nodes of a component, fold its direct children only, inspect
|
||||
the source code of the component, send it as a global variable in the console, go to the Elements tab
|
||||
and focus on its content, force a rerender of the component, send its observed states to the console
|
||||
as a global variable, inspect its compiled template in the Sources tab or send its raw template
|
||||
to the console.
|
||||
|
||||
<img src="screenshots/menu.png"/>
|
||||
|
||||
The component details window in the right will show the component that is currently selected as well
|
||||
as its env, props, observed states and all the other variables that are present on its instance.
|
||||
While the props and the env are already present on the actual instance of the component and are
|
||||
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
||||
|
||||
The observed state is actually information about which variables are observed by the component
|
||||
which will trigger a rerender of the component when it is modified. The keys represent which part of the
|
||||
variable is actually observed and the target is the actual variable. For simplicity, the properties
|
||||
that are not observed by the component are greyed out while the others are in bold. This means that
|
||||
editing bold ones will trigger a rerender while the greyed out ones will not.
|
||||
|
||||
<img src="screenshots/states.png"/>
|
||||
|
||||
In the given example, we have two keys/target pairs for two different variables. The first one indicates
|
||||
that adding or removing an element to the array will trigger a rerender since the length will have changed.
|
||||
Replacing the element at index 0, 1 or 2 will also have the same effect as implied by the keys. It doesn't
|
||||
mean that editing the properties of element at index 0, 1 or 2 will rerender the component though. It may
|
||||
be the case for some but this will be described in another keys/target pair. The second keys/target pair
|
||||
is actually the element at index 0 of the first pair. It only has id in the keys meaning that only the
|
||||
id property will actually trigger a rerender the component when modified. Be aware however that the other
|
||||
properties may be in the observed state of another component like a child one in this case. A greyed out
|
||||
property only implies it is not reactive for the selected component and not for the others.
|
||||
|
||||
The navigation inside the properties is also similar to the one in console variables: properties have
|
||||
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
||||
send any property to the console using the right-click context menu on it and functions can be inspected
|
||||
in the sources tab as well.
|
||||
|
||||
<img src="screenshots/function_menu.png"/>
|
||||
|
||||
There are several icons available to perform several of the actions described before in the components
|
||||
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
||||
component's name. Using the left click on the component's name will focus it in the components tree.
|
||||
|
||||
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
||||
property's value and modify it using the freshly created input then press enter to apply the changes.
|
||||
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Whether it has an impact on the
|
||||
component or not and whether it produces an error is the responsability of the user.
|
||||
|
||||
<img src="screenshots/edit.png"/>
|
||||
|
||||
## Profiler
|
||||
|
||||
The profiler tab is the other tab of the owl devtools. It consists in an actions bar at the top and
|
||||
a tree/list of events related to the owl components' renders. Here is an example of the events launched
|
||||
when entering the Odoo Crm app.
|
||||
|
||||
<img src="screenshots/profiler.png"/>
|
||||
|
||||
In the initial state, no event is displayed. You need to activate the recording of events before they
|
||||
are intercepted by the devtools using the record button.
|
||||
|
||||
<img src="screenshots/record.png"/>
|
||||
|
||||
The second button is used to clear all the events that have been recorded. The select can be used to
|
||||
switch between the tree view (which shows the causality between renders) and the events log view which
|
||||
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
||||
update and destroy events which reveals the component that initiated the event.
|
||||
|
||||
<img src="screenshots/events_log.png"/>
|
||||
|
||||
The third button is only visible in tree view and allows to fold all the render events that were recorded.
|
||||
Some actions are also available when using the right-click on any event of the tree view for navigation
|
||||
purpose in a similar fashion as in the components tree.
|
||||
|
||||
<img src="screenshots/tree_actions.png"/>
|
||||
|
||||
There is also the Trace Renderings and Trace Subscriptions features. These features are independant of the
|
||||
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
||||
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
||||
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
||||
of the modification
|
||||
|
||||
<img src="screenshots/trace_rendering.png"/>
|
||||
<img src="screenshots/trace_subscriptions.png"/>
|
||||
|
||||
## Options
|
||||
|
||||
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
||||
be toggled using the sun/moon icon at the top-right corner of the tab. All the examples above were created
|
||||
with the dark mode enabled. There is also a refresh button to completely reset the owl devtools.
|
||||
|
||||
<img src="screenshots/darkmode.png"/>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the feedback from the page to the devtools seems to be cut, just close the devtools and refresh the page.
|
||||
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
||||
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 146 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.0.9",
|
||||
"version": "2.1.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
@@ -19,8 +19,8 @@
|
||||
"build:devtools": "rollup -c ./tools/devtools/rollup.config.js",
|
||||
"dev:devtools-chrome": "npm run build:devtools -- --config-browser=chrome",
|
||||
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
|
||||
"build:devtools-chrome": "NODE_ENV=production npm run dev:devtools-chrome",
|
||||
"build:devtools-firefox": "NODE_ENV=production npm run dev:devtools-firefox",
|
||||
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
|
||||
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
|
||||
"test": "jest",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||
"test:watch": "jest --watch",
|
||||
|
||||
@@ -84,7 +84,10 @@ export class App<
|
||||
this.props = config.props || ({} as P);
|
||||
}
|
||||
|
||||
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
|
||||
mount(
|
||||
target: HTMLElement | ShadowRoot,
|
||||
options?: MountOptions
|
||||
): Promise<Component<P, E> & InstanceType<T>> {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||
@@ -99,7 +102,7 @@ export class App<
|
||||
return new ComponentNode(Component, props, this, null, null);
|
||||
}
|
||||
|
||||
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
||||
mountNode(node: ComponentNode, target: HTMLElement | ShadowRoot, options?: MountOptions) {
|
||||
const promise: any = new Promise((resolve, reject) => {
|
||||
let isResolved = false;
|
||||
// manually set a onMounted callback.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { inOwnerDocument } from "../utils";
|
||||
import { config } from "./config";
|
||||
|
||||
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||
@@ -28,7 +29,7 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
|
||||
|
||||
function listener(ev: Event) {
|
||||
const currentTarget = ev.currentTarget as HTMLElement;
|
||||
if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget)) return;
|
||||
if (!currentTarget || !inOwnerDocument(currentTarget)) return;
|
||||
const data = (currentTarget as any)[eventKey];
|
||||
if (!data) return;
|
||||
config.mainEventHandler(data, ev, currentTarget);
|
||||
|
||||
@@ -73,6 +73,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
forceNextRender: boolean = false;
|
||||
parentKey: string | null;
|
||||
props: P;
|
||||
nextProps: P | null = null;
|
||||
|
||||
renderFn: Function;
|
||||
parent: ComponentNode | null;
|
||||
@@ -220,7 +221,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
}
|
||||
|
||||
async updateAndRender(props: P, parentFiber: Fiber) {
|
||||
const rawProps = props;
|
||||
this.nextProps = props;
|
||||
props = Object.assign({}, props);
|
||||
// update
|
||||
const fiber = makeChildFiber(this, parentFiber);
|
||||
@@ -245,7 +246,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
return;
|
||||
}
|
||||
component.props = props;
|
||||
this.props = rawProps;
|
||||
fiber.render();
|
||||
const parentRoot = parentFiber.root!;
|
||||
if (this.willPatch.length) {
|
||||
@@ -327,6 +327,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
// by the component will be patched independently in the appropriate
|
||||
// fiber.complete
|
||||
this._patch();
|
||||
this.props = this.nextProps!;
|
||||
}
|
||||
}
|
||||
_patch() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Env } from "./app";
|
||||
import { getCurrent } from "./component_node";
|
||||
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
|
||||
import { inOwnerDocument } from "./utils";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useRef
|
||||
@@ -16,7 +17,7 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
|
||||
return {
|
||||
get el(): T | null {
|
||||
const el = refs[name];
|
||||
return el?.ownerDocument.contains(el) ? el : null;
|
||||
return inOwnerDocument(el) ? el : null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export {
|
||||
onWillDestroy,
|
||||
onError,
|
||||
} from "./lifecycle_hooks";
|
||||
export { validate } from "./validation";
|
||||
export { validate, validateType } from "./validation";
|
||||
export { OwlError } from "./error_handling";
|
||||
|
||||
export const __info__ = {
|
||||
|
||||
@@ -27,13 +27,28 @@ export function batched(callback: Callback): Callback {
|
||||
};
|
||||
}
|
||||
|
||||
export function validateTarget(target: HTMLElement) {
|
||||
/**
|
||||
* Determine whether the given element is contained in its ownerDocument:
|
||||
* either directly or with a shadow root in between.
|
||||
*/
|
||||
export function inOwnerDocument(el?: HTMLElement) {
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
if (el.ownerDocument.contains(el)) {
|
||||
return true;
|
||||
}
|
||||
const rootNode = el.getRootNode();
|
||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||
}
|
||||
|
||||
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||
const document = target && target.ownerDocument;
|
||||
if (document) {
|
||||
const HTMLElement = document.defaultView!.HTMLElement;
|
||||
if (target instanceof HTMLElement) {
|
||||
if (!document.body.contains(target)) {
|
||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,7 @@ interface TypeInfo {
|
||||
validate?: Function;
|
||||
shape?: Schema;
|
||||
element?: TypeDescription;
|
||||
values?: TypeDescription;
|
||||
}
|
||||
|
||||
type ValueType = { value: any };
|
||||
@@ -137,7 +138,7 @@ function validateArrayType(key: string, value: any, descr: TypeDescription): str
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateType(key: string, value: any, descr: TypeDescription): string | null {
|
||||
export function validateType(key: string, value: any, descr: TypeDescription): string | null {
|
||||
if (value === undefined) {
|
||||
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
|
||||
} else if (isBaseType(descr)) {
|
||||
@@ -151,13 +152,24 @@ function validateType(key: string, value: any, descr: TypeDescription): string |
|
||||
let result: string | null = null;
|
||||
if ("element" in descr) {
|
||||
result = validateArrayType(key, value, descr.element!);
|
||||
} else if ("shape" in descr && !result) {
|
||||
} else if ("shape" in descr) {
|
||||
if (typeof value !== "object" || Array.isArray(value)) {
|
||||
result = `'${key}' is not an object`;
|
||||
} else {
|
||||
const errors = validateSchema(value, descr.shape!);
|
||||
if (errors.length) {
|
||||
result = `'${key}' has not the correct shape (${errors.join(", ")})`;
|
||||
result = `'${key}' doesn't have the correct shape (${errors.join(", ")})`;
|
||||
}
|
||||
}
|
||||
} else if ("values" in descr) {
|
||||
if (typeof value !== "object" || Array.isArray(value)) {
|
||||
result = `'${key}' is not an object`;
|
||||
} else {
|
||||
const errors = Object.entries(value)
|
||||
.map(([key, value]) => validateType(key, value, descr.values!))
|
||||
.filter(Boolean);
|
||||
if (errors.length) {
|
||||
result = `some of the values in '${key}' are invalid (${errors.join(", ")})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This value is updated by the release script.
|
||||
export const version = "2.1.0";
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.1.1";
|
||||
|
||||
@@ -1252,6 +1252,31 @@ exports[`delayed render does not go through when t-component value changed 3`] =
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`delayed render is not cancelled by upcoming render 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`B\`, true, false, false, [\\"state\\",\\"isEmpty\\"]);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({state: ctx['state'],isEmpty: ctx['state'].groups.length===0}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`delayed render is not cancelled by upcoming render 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(ctx['props'].state.groups.length);
|
||||
const b3 = text(ctx['props'].state.config.test);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -4126,6 +4126,80 @@ test("delayed render does not go through when t-component value changed", async
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test("delayed render is not cancelled by upcoming render", async () => {
|
||||
let b: any;
|
||||
|
||||
class B extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="props.state.groups.length"/>
|
||||
<t t-esc="props.state.config.test"/>`;
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
b = this;
|
||||
}
|
||||
}
|
||||
|
||||
class A extends Component {
|
||||
static components = { B };
|
||||
static template = xml`<B state="state" isEmpty="state.groups.length === 0"/>`;
|
||||
|
||||
state = useState({ groups: [], config: { test: "initial" } });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
await mount(A, fixture);
|
||||
|
||||
expect([
|
||||
"A:setup",
|
||||
"A:willStart",
|
||||
"A:willRender",
|
||||
"B:setup",
|
||||
"B:willStart",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"B:rendered",
|
||||
"B:mounted",
|
||||
"A:mounted",
|
||||
]).toBeLogged();
|
||||
|
||||
expect(fixture.innerHTML).toBe("0initial");
|
||||
|
||||
b.props.state.config.test = "red";
|
||||
b.props.state.groups.push(1);
|
||||
expect([]).toBeLogged();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
b.props.state.config.test = "black";
|
||||
b.props.state.groups.push(1);
|
||||
expect([
|
||||
"A:willRender",
|
||||
"B:willUpdateProps",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"B:rendered",
|
||||
]).toBeLogged();
|
||||
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("2black");
|
||||
expect([
|
||||
"A:willRender",
|
||||
"B:willUpdateProps",
|
||||
"A:rendered",
|
||||
"B:willRender",
|
||||
"B:rendered",
|
||||
"A:willPatch",
|
||||
"B:willPatch",
|
||||
"B:patched",
|
||||
"A:patched",
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
// test.skip("components with shouldUpdate=false", async () => {
|
||||
// const state = { p: 1, cc: 10 };
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape (unknown key 'extra')"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape (unknown key 'extra')"
|
||||
);
|
||||
props = { p: { id: "1", url: "url" } };
|
||||
app = new App(Parent, { test: true });
|
||||
@@ -413,7 +413,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape ('id' is not a number)"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape ('id' is not a number)"
|
||||
);
|
||||
error = undefined;
|
||||
props = { p: { id: 1 } };
|
||||
@@ -423,7 +423,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is missing (should be a string))"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape ('url' is missing (should be a string))"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -470,7 +470,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is not a boolean or list of numbers)"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape ('url' is not a boolean or list of numbers)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -502,7 +502,7 @@ describe("props validation", () => {
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'TestComponent': 'myprop[0]' has not the correct shape (unknown key 'a')"
|
||||
"Invalid props for component 'TestComponent': 'myprop[0]' doesn't have the correct shape (unknown key 'a')"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`shadow_dom can bind event handler 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['add'], ctx];
|
||||
return block1([hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`shadow_dom can mount app 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`shadow_dom useRef hook 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"my-div\\" block-ref=\\"0\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let ref1 = (el) => this.__owl__.setRef((\`refName\`), el);
|
||||
return block1([ref1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { App, Component, useRef, xml } from "../../src";
|
||||
import { status } from "../../src/runtime/status";
|
||||
import { makeTestFixture, snapshotEverything } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
snapshotEverything();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
describe("shadow_dom", () => {
|
||||
test("can mount app", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div class="my-div"/>`;
|
||||
}
|
||||
|
||||
const container = document.createElement("div");
|
||||
fixture.appendChild(container);
|
||||
const shadow = container.attachShadow({ mode: "open" });
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(shadow);
|
||||
const div = shadow.querySelector(".my-div");
|
||||
expect(div).not.toBe(null);
|
||||
expect(shadow.contains(div)).toBe(true);
|
||||
app.destroy();
|
||||
expect(shadow.contains(div)).toBe(false);
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("can bind event handler", async () => {
|
||||
let a = 1;
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<button t-on-click="add">Click</button>`;
|
||||
|
||||
add() {
|
||||
a = 3;
|
||||
}
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
fixture.appendChild(container);
|
||||
const shadow = container.attachShadow({ mode: "open" });
|
||||
await new App(SomeComponent).mount(shadow);
|
||||
expect(a).toBe(1);
|
||||
shadow.querySelector("button")!.click();
|
||||
expect(a).toBe(3);
|
||||
});
|
||||
|
||||
test("useRef hook", async () => {
|
||||
let comp: SomeComponent;
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div t-ref="refName" class="my-div"/>`;
|
||||
div = useRef("refName");
|
||||
setup() {
|
||||
comp = this;
|
||||
}
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
fixture.appendChild(container);
|
||||
const shadow = container.attachShadow({ mode: "open" });
|
||||
const mountedProm = new App(SomeComponent).mount(shadow);
|
||||
expect(comp!.div.el).toBe(null);
|
||||
await mountedProm;
|
||||
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
||||
});
|
||||
});
|
||||
@@ -92,6 +92,10 @@ export function isLinkValid(link: MarkDownLink, current: FileData, files: FileDa
|
||||
// no check on external links
|
||||
return true;
|
||||
}
|
||||
if (current.name.endsWith(".png")) {
|
||||
// no check on png files
|
||||
return true;
|
||||
}
|
||||
// Step 1: extract path, name, hash
|
||||
// path = ['doc', 'architecture]
|
||||
// name = 'rendering.md'
|
||||
|
||||
@@ -145,17 +145,51 @@ describe("validateSchema", () => {
|
||||
const schema: Schema = { p: { type: Object, shape: { id: Number, url: String } } };
|
||||
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||
expect(validateSchema({ p: {} }, schema)).toEqual([
|
||||
"'p' has not the correct shape ('id' is missing (should be a number), 'url' is missing (should be a string))",
|
||||
"'p' doesn't have the correct shape ('id' is missing (should be a number), 'url' is missing (should be a string))",
|
||||
]);
|
||||
expect(validateSchema({ p: { id: 1, url: "asf" } }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: { id: 1, url: 1 } }, schema)).toEqual([
|
||||
"'p' has not the correct shape ('url' is not a string)",
|
||||
"'p' doesn't have the correct shape ('url' is not a string)",
|
||||
]);
|
||||
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||
"'p' is undefined (should be a object)",
|
||||
]);
|
||||
});
|
||||
|
||||
test("objects with a values schema", () => {
|
||||
const schema: Schema = {
|
||||
p: { type: Object, values: { type: Object, shape: { id: Number, url: String } } },
|
||||
};
|
||||
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||
expect(validateSchema({ p: {} }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: { id: 1, url: "asf" } }, schema)).toEqual([
|
||||
"some of the values in 'p' are invalid ('id' is not an object, 'url' is not an object)",
|
||||
]);
|
||||
expect(
|
||||
validateSchema(
|
||||
{
|
||||
p: {
|
||||
a: { id: 1, url: "asf" },
|
||||
},
|
||||
},
|
||||
schema
|
||||
)
|
||||
).toEqual([]);
|
||||
expect(
|
||||
validateSchema(
|
||||
{
|
||||
p: {
|
||||
a: { id: 1, url: "asf" },
|
||||
b: { id: 1, url: 1 },
|
||||
},
|
||||
},
|
||||
schema
|
||||
)
|
||||
).toEqual([
|
||||
"some of the values in 'p' are invalid ('b' doesn't have the correct shape ('url' is not a string))",
|
||||
]);
|
||||
});
|
||||
|
||||
test("objects with more complex shape", () => {
|
||||
const schema: Schema = {
|
||||
p: {
|
||||
@@ -168,10 +202,10 @@ describe("validateSchema", () => {
|
||||
};
|
||||
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||
expect(validateSchema({ p: {} }, schema)).toEqual([
|
||||
"'p' has not the correct shape ('id' is missing (should be a number), 'url' is missing (should be a boolean or list of numbers))",
|
||||
"'p' doesn't have the correct shape ('id' is missing (should be a number), 'url' is missing (should be a boolean or list of numbers))",
|
||||
]);
|
||||
expect(validateSchema({ p: { id: 1, url: "asf" } }, schema)).toEqual([
|
||||
"'p' has not the correct shape ('url' is not a boolean or list of numbers)",
|
||||
"'p' doesn't have the correct shape ('url' is not a boolean or list of numbers)",
|
||||
]);
|
||||
expect(validateSchema({ p: { id: 1, url: true } }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||
@@ -183,11 +217,11 @@ describe("validateSchema", () => {
|
||||
const schema: Schema = { p: { type: Object, shape: { id: Number, "*": true } } };
|
||||
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||
expect(validateSchema({ p: {} }, schema)).toEqual([
|
||||
"'p' has not the correct shape ('id' is missing (should be a number))",
|
||||
"'p' doesn't have the correct shape ('id' is missing (should be a number))",
|
||||
]);
|
||||
expect(validateSchema({ p: { id: 1 } }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: { id: "asdf" } }, schema)).toEqual([
|
||||
"'p' has not the correct shape ('id' is not a number)",
|
||||
"'p' doesn't have the correct shape ('id' is not a number)",
|
||||
]);
|
||||
expect(validateSchema({ p: { id: 1, url: 1 } }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||
@@ -222,7 +256,7 @@ describe("validateSchema", () => {
|
||||
expect(validateSchema({ p: [{}] }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: [{ num: 1 }] }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: [{ num: true }] }, schema)).toEqual([
|
||||
"'p[0]' has not the correct shape ('num' is not a number)",
|
||||
"'p[0]' doesn't have the correct shape ('num' is not a number)",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import execute from "rollup-plugin-execute";
|
||||
import del from "rollup-plugin-delete";
|
||||
import { string } from "rollup-plugin-string";
|
||||
|
||||
export default ({ "config-browser": browser }) => {
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
export default ({ "config-browser": browser, "config-env": env }) => {
|
||||
const isProduction = env === "production";
|
||||
const isChrome = browser === "chrome";
|
||||
const filesToMove = [
|
||||
{ src: "tools/devtools/assets/**/*", dest: "dist/devtools/assets/" },
|
||||
@@ -53,13 +55,19 @@ export default ({ "config-browser": browser }) => {
|
||||
};
|
||||
}
|
||||
const commands = new Array(2);
|
||||
commands[1] =
|
||||
"npm run compile_templates -- tools/devtools/src && mv templates.js tools/devtools/assets/templates.js";
|
||||
commands[1] = isWindows
|
||||
? "npm run compile_templates -- tools\\devtools\\src && move templates.js tools\\devtools\\assets\\templates.js"
|
||||
: "npm run compile_templates -- tools/devtools/src && mv templates.js tools/devtools/assets/templates.js";
|
||||
const firstRule = generateRule("tools/devtools/src/page_scripts/owl_devtools_global_hook.js");
|
||||
if (isProduction)
|
||||
commands[0] =
|
||||
"npm run build && cp dist/owl.iife.js tools/devtools/assets/owl.js && npm run build:compiler";
|
||||
else commands[0] = "cp dist/owl.iife.js tools/devtools/assets/owl.js";
|
||||
if (isProduction) {
|
||||
commands[0] = isWindows
|
||||
? "npm run build && copy dist\\owl.iife.js tools\\devtools\\assets\\owl.js && npm run build:compiler"
|
||||
: "npm run build && cp dist/owl.iife.js tools/devtools/assets/owl.js && npm run build:compiler";
|
||||
} else {
|
||||
commands[0] = isWindows
|
||||
? "copy dist\\owl.iife.js tools\\devtools\\assets\\owl.js"
|
||||
: "cp dist/owl.iife.js tools/devtools/assets/owl.js";
|
||||
}
|
||||
firstRule.plugins.push(execute(commands, true));
|
||||
const secondRule = generateRule("tools/devtools/src/content.js");
|
||||
secondRule.plugins.push(copy({ targets: filesToMove }));
|
||||
|
||||
@@ -77,6 +77,15 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
|
||||
setOwlStatus(message.data);
|
||||
// Dummy message to test if the extension context is still valid
|
||||
} else if (message.type === "keepAlive") {
|
||||
return;
|
||||
// Open the devtools documentation in a new tab
|
||||
} else if (message.type === "openDoc") {
|
||||
browserInstance.tabs.create(
|
||||
{ url: "https://github.com/odoo/owl/blob/master/doc/tools/devtools_guide.md", active: false },
|
||||
function (tab) {
|
||||
browserInstance.tabs.update(tab.id, { active: true });
|
||||
}
|
||||
);
|
||||
return;
|
||||
// Relay the received message to the devtools app
|
||||
} else if (message.type === "newDevtoolsPanel") {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
<option t-att-value="frame"><t t-esc="frame"/></option>
|
||||
</t>
|
||||
</select>
|
||||
<i class="ms-auto p-1 me-1 lg-icon fa pointer-icon" title="Toggle dark mode" t-att-class="{ 'fa-sun-o': store.settings.darkMode, 'fa-moon-o' : !store.settings.darkMode}" t-on-click.stop="() => this.store.toggleDarkMode()"></i>
|
||||
<i class="ms-auto p-1 me-1 lg-icon fa fa-question-circle pointer-icon" title="Open devtools doc" t-on-click.stop="() => this.store.openDocumentation()"></i>
|
||||
<i class="p-1 me-1 lg-icon fa pointer-icon" title="Toggle dark mode" t-att-class="{ 'fa-sun-o': store.settings.darkMode, 'fa-moon-o' : !store.settings.darkMode}" t-on-click.stop="() => this.store.toggleDarkMode()"></i>
|
||||
<i class="p-1 me-1 lg-icon fa fa-repeat pointer-icon" title="Refresh extension" t-on-click.stop="() => this.store.refreshExtension()"></i>
|
||||
</div>
|
||||
<ComponentsTab t-if="store.page === 'ComponentsTab'"/>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<option t-att-selected="store.eventsTreeView" value="Tree">Tree view</option>
|
||||
<option t-att-selected="!store.eventsTreeView" value="List">Events log</option>
|
||||
</select>
|
||||
<i title="Collapse All" class="fa fa-list me-1" t-on-click="() => this.store.collapseAll()"></i>
|
||||
<i title="Collapse All" type="button" class="fa fa-list me-2" t-on-click="() => this.store.collapseAll()" t-attf-style="{{store.eventsTreeView ? '' : 'visibility: hidden;'}}"></i>
|
||||
<div class="icons-separator"/>
|
||||
<label class="mx-2 form-check-label pointer-icon" title="Trace renderings in console">
|
||||
<input type="checkbox" class="form-check-input me-1" t-att-checked="store.traceRenderings" t-on-input="() => this.store.toggleTracing()"/> Trace Renderings
|
||||
|
||||
@@ -618,6 +618,10 @@ export const store = reactive({
|
||||
}
|
||||
browserInstance.storage.local.set({ owl_devtools_dark_mode: this.settings.darkMode });
|
||||
},
|
||||
|
||||
openDocumentation() {
|
||||
browserInstance.runtime.sendMessage({ type: "openDoc" });
|
||||
},
|
||||
});
|
||||
|
||||
// Instantiate the store
|
||||
|
||||
@@ -236,6 +236,7 @@
|
||||
.search-input {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
padding: 0.4rem 0rem;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
|
||||
@@ -507,6 +507,7 @@
|
||||
highlight.style.left = `${left}px`;
|
||||
highlight.style.width = `${width}px`;
|
||||
highlight.style.height = `${height}px`;
|
||||
highlight.style.boxSizing = "border-box";
|
||||
highlight.style.position = "fixed";
|
||||
highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)";
|
||||
highlight.style.borderStyle = "solid";
|
||||
@@ -524,6 +525,7 @@
|
||||
highlightMargins.style.height = `${height + marginBottom + marginTop}px`;
|
||||
highlightMargins.style.position = "fixed";
|
||||
highlightMargins.style.borderStyle = "solid";
|
||||
highlightMargins.style.boxSizing = "border-box";
|
||||
highlightMargins.style.borderWidth = `${marginTop}px ${marginRight}px ${marginBottom}px ${marginLeft}px`;
|
||||
highlightMargins.style.borderColor = "rgba(241, 179, 121, 0.4)";
|
||||
highlightMargins.style.zIndex = "10000";
|
||||
@@ -537,6 +539,7 @@
|
||||
highlight.style.left = `${left}px`;
|
||||
highlight.style.width = `${width}px`;
|
||||
highlight.style.height = `${height}px`;
|
||||
highlight.style.boxSizing = "border-box";
|
||||
highlight.style.position = "fixed";
|
||||
highlight.style.backgroundColor = "rgba(15, 139, 245, 0.4)";
|
||||
highlight.style.zIndex = "10000";
|
||||
|
||||
@@ -8,8 +8,9 @@ export async function getOwlStatus() {
|
||||
}
|
||||
|
||||
export async function getActiveTabURL() {
|
||||
const [tab] = await browserInstance.tabs.query({ active: true, lastFocusedWindow: true });
|
||||
return tab.id;
|
||||
const window = await browserInstance.windows.getLastFocused({ populate: true });
|
||||
const activeTab = window.tabs.find((tab) => tab.active);
|
||||
return activeTab.id;
|
||||
}
|
||||
|
||||
// inspired from https://www.tutorialspoint.com/fuzzy-search-algorithm-in-javascript
|
||||
|
||||
@@ -226,6 +226,15 @@ class App extends owl.Component {
|
||||
this.downloadCode = debounce(this.downloadCode, 250, true);
|
||||
this.content = useRef("content");
|
||||
this.updateCode = this.updateCode.bind(this);
|
||||
if (window.location.hash) {
|
||||
try {
|
||||
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
|
||||
if (![js, css, xml].every(item => typeof item === "string")) {
|
||||
return;
|
||||
}
|
||||
Object.assign(this.state, { js, css, xml });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
runCode() {
|
||||
@@ -237,6 +246,18 @@ class App extends owl.Component {
|
||||
this.content.el.appendChild(subiframe);
|
||||
}
|
||||
|
||||
shareCode() {
|
||||
const state = btoa(JSON.stringify({ js: this.state.js, css: this.state.css, xml: this.state.xml }));
|
||||
const link = new URL(window.location.href);
|
||||
link.hash = state;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(link.href);
|
||||
clearTimeout(this.state.copied)
|
||||
this.state.copied = setTimeout(() => this.state.copied = null, 2000);
|
||||
}
|
||||
window.location.href = link.href;
|
||||
}
|
||||
|
||||
setSample(ev) {
|
||||
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
|
||||
this.state.js = sample.code;
|
||||
|
||||
@@ -79,6 +79,19 @@ body {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.share-code {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copied-notification {
|
||||
font-size: x-small;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
white-space: nowrap;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* TABBED EDITOR ****************************************/
|
||||
|
||||
.tabbed-editor {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</select>
|
||||
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
|
||||
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
|
||||
<a class="btn flash share-code" t-on-click="shareCode" title="Copy a link to this playground"><span>Share</span><span class="copied-notification" t-if="state.copied">Copied link to clipboard!</span></a>
|
||||
</div>
|
||||
<TabbedEditor
|
||||
js="state.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const package = require("../package.json");
|
||||
const packageLock = require("../package-lock.json");
|
||||
const readline = require("readline");
|
||||
const fs = require("fs");
|
||||
const exec = require("child_process").exec;
|
||||
@@ -41,7 +42,6 @@ async function startRelease() {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
log(`Step 1/${STEPS}: collecting info...`);
|
||||
const current = package.version;
|
||||
let next = await ask("Next version: ");
|
||||
if (next[0] === 'v') next = next.substring(1);
|
||||
let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
|
||||
@@ -85,8 +85,9 @@ async function startRelease() {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
log(`Step 3/${STEPS}: updating package.json...`);
|
||||
await replaceInFile("./package.json", current, next);
|
||||
await replaceInFile("./src/version.ts", current, next);
|
||||
await writeFile("package.json", JSON.stringify({...package, version: next}, null, 2) + "\n");
|
||||
await writeFile("package-lock.json", JSON.stringify({...packageLock, version: next}, null, 2) + "\n");
|
||||
await writeFile("./src/version.ts", `// do not modify manually. This file is generated by the release script.\nexport const version = "${next}";\n`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
log(`Step 4/${STEPS}: creating git commit...`);
|
||||
@@ -246,11 +247,9 @@ function readFile(file) {
|
||||
});
|
||||
}
|
||||
|
||||
async function replaceInFile(file, from, to) {
|
||||
const content = await readFile(file);
|
||||
function writeFile(file, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const updatedContent = content.replace(new RegExp(from, "g"), to);
|
||||
fs.writeFile(file, updatedContent, "utf8", err => {
|
||||
fs.writeFile(file, content, "utf8", err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
|
||||