mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea5d2be502 | |||
| 4a761a7403 | |||
| 8702db03fb | |||
| b2685b6709 | |||
| c30678f3ea | |||
| 6ca6717965 | |||
| ad4adb930e | |||
| cea82e945d | |||
| a69f8a39e7 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.5",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -632,7 +632,15 @@ export class CodeGenerator {
|
||||
|
||||
let idx: number;
|
||||
if (specialInitTargetAttr) {
|
||||
idx = block!.insertData(`${fullExpression} === '${attrs[targetAttr]}'`, "attr");
|
||||
let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;
|
||||
if (!targetExpr && ast.attrs) {
|
||||
// look at the dynamic attribute counterpart
|
||||
const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];
|
||||
if (dynamicTgExpr) {
|
||||
targetExpr = compileExpr(dynamicTgExpr);
|
||||
}
|
||||
}
|
||||
idx = block!.insertData(`${fullExpression} === ${targetExpr}`, "attr");
|
||||
attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
|
||||
} else if (hasDynamicChildren) {
|
||||
const bValueId = generateId("bValue");
|
||||
|
||||
@@ -31,6 +31,18 @@ This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__OWL_DEVTOOLS__: {
|
||||
apps: Set<App>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.__OWL_DEVTOOLS__ ||= {
|
||||
apps: new Set<App>(),
|
||||
};
|
||||
|
||||
export class App<
|
||||
T extends abstract new (...args: any) => any = any,
|
||||
P extends object = any,
|
||||
@@ -48,6 +60,7 @@ export class App<
|
||||
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
|
||||
super(config);
|
||||
this.Root = Root;
|
||||
window.__OWL_DEVTOOLS__.apps.add(this);
|
||||
if (config.test) {
|
||||
this.dev = true;
|
||||
}
|
||||
@@ -109,6 +122,7 @@ export class App<
|
||||
this.scheduler.flush();
|
||||
this.root.destroy();
|
||||
}
|
||||
window.__OWL_DEVTOOLS__.apps.delete(this);
|
||||
}
|
||||
|
||||
createComponent<P extends Props>(
|
||||
|
||||
@@ -117,7 +117,7 @@ export function useEffect(effect: Effect, computeDependencies: () => any[] = ()
|
||||
* `useExternalListener(window, 'click', this._doSomething);`
|
||||
* */
|
||||
export function useExternalListener(
|
||||
target: HTMLElement | typeof window,
|
||||
target: EventTarget,
|
||||
eventName: string,
|
||||
handler: EventListener,
|
||||
eventParams?: AddEventListenerOptions
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Callback } from "./utils";
|
||||
import type { Callback } from "./utils";
|
||||
import { OwlError } from "./error_handling";
|
||||
|
||||
// Special key to subscribe to, to be notified of key creation/deletion
|
||||
const KEYCHANGES = Symbol("Key changes");
|
||||
// Used to specify the absence of a callback, can be used as WeakMap key but
|
||||
// should only be used as a sentinel value and never called.
|
||||
const NO_CALLBACK = () => {
|
||||
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
|
||||
};
|
||||
|
||||
// The following types only exist to signify places where objects are expected
|
||||
// to be reactive or not, they provide no type checking benefit over "object"
|
||||
@@ -86,6 +91,9 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
|
||||
* @param callback the function to call when the key changes
|
||||
*/
|
||||
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
|
||||
if (callback === NO_CALLBACK) {
|
||||
return;
|
||||
}
|
||||
if (!targetToKeysToCallbacks.get(target)) {
|
||||
targetToKeysToCallbacks.set(target, new Map());
|
||||
}
|
||||
@@ -140,8 +148,11 @@ export function clearReactivesForCallback(callback: Callback): void {
|
||||
if (!observedKeys) {
|
||||
continue;
|
||||
}
|
||||
for (const callbacks of observedKeys.values()) {
|
||||
for (const [key, callbacks] of observedKeys.entries()) {
|
||||
callbacks.delete(callback);
|
||||
if (!callbacks.size) {
|
||||
observedKeys.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
targetsToClear.clear();
|
||||
@@ -187,7 +198,7 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>()
|
||||
* reactive has changed
|
||||
* @returns a proxy that tracks changes to it
|
||||
*/
|
||||
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): T {
|
||||
export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): T {
|
||||
if (!canBeMadeReactive(target)) {
|
||||
throw new OwlError(`Cannot make the given value reactive`);
|
||||
}
|
||||
|
||||
@@ -569,6 +569,36 @@ exports[`t-model directive t-model with dynamic values on select options in fore
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive t-model with radio button group in t-foreach 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { prepareList, toNumber, withKey } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div id=\\"get_data\\" block-handler-0=\\"click\\"><block-child-0/></div>\`);
|
||||
let block3 = createBlock(\`<input type=\\"radio\\" name=\\"radio_group\\" block-attribute-0=\\"value\\" block-attribute-1=\\"id\\" block-attribute-2=\\"checked\\" block-handler-3=\\"click\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['getData'], ctx];
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['options']);;
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`opt\`] = v_block2[i1];
|
||||
const key1 = ctx['opt'];
|
||||
let attr1 = new String((ctx['opt']) || \\"\\");
|
||||
let attr2 = ctx['opt'];
|
||||
const bExpr1 = ctx['state'];
|
||||
const expr1 = 'group';
|
||||
let attr3 = bExpr1[expr1] === ctx['opt'];
|
||||
let hdlr2 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
|
||||
c_block2[i1] = withKey(block3([attr1, attr2, attr3, hdlr2]), key1);
|
||||
}
|
||||
const b2 = list(c_block2);
|
||||
return block1([hdlr1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive two inputs in a div alternating with a t-if 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -644,4 +644,35 @@ describe("t-model directive", () => {
|
||||
const input = fixture.querySelector("input")!;
|
||||
await editInput(input, "Beam me up, Scotty");
|
||||
});
|
||||
|
||||
test("t-model with radio button group in t-foreach", async () => {
|
||||
expect.assertions(6);
|
||||
const steps: string[] = [];
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="getData" id="get_data">
|
||||
<t t-foreach="options" t-as="opt" t-key="opt">
|
||||
<input type="radio" name="radio_group" t-model="state.group" t-att-value="opt" t-att-id="opt"/>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
||||
state = useState({ group: "scotty" });
|
||||
options = ["beam", "scotty"];
|
||||
|
||||
getData() {
|
||||
steps.push(`group: ${this.state.group}`);
|
||||
}
|
||||
}
|
||||
await mount(SomeComponent, fixture);
|
||||
const divEl = fixture.querySelector("#get_data") as HTMLElement;
|
||||
expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("scotty");
|
||||
divEl.click();
|
||||
expect(steps).toEqual(["group: scotty"]);
|
||||
fixture.querySelector("input")!.click();
|
||||
expect(steps).toEqual(["group: scotty", "group: beam"]);
|
||||
await nextTick();
|
||||
expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("beam");
|
||||
divEl.click();
|
||||
expect(steps).toEqual(["group: scotty", "group: beam", "group: beam"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ function writeToFile(filepath, data) {
|
||||
}
|
||||
|
||||
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||
const a = "·_,:;";
|
||||
const a = "·-_,:;";
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
|
||||
function slugify(str) {
|
||||
|
||||
+35
-6
@@ -47,6 +47,18 @@ async function startRelease() {
|
||||
let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
|
||||
file = file || REL_NOTES_FILE;
|
||||
let content;
|
||||
if (!fs.existsSync(`./${file}`)) {
|
||||
let lastRelease = await getOutput("git log --grep='\\[REL\\]' -n 1 --pretty=%H");
|
||||
const commitsSinceLastRelease = await getOutput(`git log ${lastRelease.trim()}..HEAD --pretty=%s`);
|
||||
const commitsAsMdList = commitsSinceLastRelease.trim().split("\n").map(l => " - " + l).join("\n");
|
||||
log(`${file} did not exist, created a template containing all commits since last release.`)
|
||||
fs.writeFileSync(file, `# v${next}\n\n${commitsAsMdList}`);
|
||||
const shouldContinue = await ask(`Check that the contents of ${file} is correct, then press y to continue: `);
|
||||
if (shouldContinue.toLowerCase() !== "y") {
|
||||
log("aborted");
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
content = await readFile("./" + file);
|
||||
} catch (e) {
|
||||
@@ -104,7 +116,7 @@ async function startRelease() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
log(`Step 7/${STEPS}: Creating the release...`);
|
||||
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F ${REL_NOTES_FILE}`);
|
||||
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F ${file}`);
|
||||
if (relaseResult !== 0) {
|
||||
logError("github release failed. Aborting.");
|
||||
return;
|
||||
@@ -120,7 +132,7 @@ async function startRelease() {
|
||||
if (shouldUploadPlayground) {
|
||||
log(`Bonus step: publishing new release on playground...`);
|
||||
let owl_code = null;
|
||||
status = 0
|
||||
let status = 0
|
||||
|
||||
try {
|
||||
owl_code = await readFile("dist/owl.iife.js");
|
||||
@@ -130,7 +142,8 @@ async function startRelease() {
|
||||
return;
|
||||
}
|
||||
|
||||
status += await execCommand("git checkout gh-pages");
|
||||
status |= await execCommand("git checkout gh-pages");
|
||||
status |= await execCommand("git pull --rebase");
|
||||
|
||||
if (status !== 0) {
|
||||
logError("Couldn't switch to gh-pages branch")
|
||||
@@ -144,9 +157,9 @@ async function startRelease() {
|
||||
return;
|
||||
}
|
||||
|
||||
status += await execCommand(`git commit -am "[IMP] update owl to v${next}"`);
|
||||
status += await execCommand(`git push origin gh-pages`);
|
||||
status += await execCommand("git checkout -");
|
||||
status |= await execCommand(`git commit -am "[IMP] update owl to v${next}"`);
|
||||
status |= await execCommand(`git push origin gh-pages`);
|
||||
status |= await execCommand("git checkout -");
|
||||
if (status !== 0) {
|
||||
logError("Something went wrong for the playground update.")
|
||||
}
|
||||
@@ -231,3 +244,19 @@ async function replaceInFile(file, from, to) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getOutput(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = exec(command, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
childProcess.on("exit", code => {
|
||||
if (code !== 0) {
|
||||
reject(code);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user