Create Todo app
Summary: Ref T2687 Reviewers: #testers, kuba-orlik Reviewed By: #testers, kuba-orlik Subscribers: kuba-orlik, jenkins-user Maniphest Tasks: T2687 Differential Revision: https://hub.sealcode.org/D1339master
							parent
							
								
									b7cc271d97
								
							
						
					
					
						commit
						0c49effa9d
					
				| @ -0,0 +1,9 @@ | |||||||
|  | import { LONG_TEST_TIMEOUT } from "./src/back/test_utils/webhint"; | ||||||
|  | import { closeBrowser } from "./src/back/test_utils/browser-creator"; | ||||||
|  | 
 | ||||||
|  | exports.mochaHooks = { | ||||||
|  | 	async afterAll() { | ||||||
|  | 		this.timeout(LONG_TEST_TIMEOUT); | ||||||
|  | 		await closeBrowser(); | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
| @ -1,12 +1,14 @@ | |||||||
| #!/usr/bin/env -S bash -x | #!/usr/bin/env -S bash -x | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| # the "--no-TTY" option is crucial - without it the output is not captured in Jenkins | # the "--no-TTY" option is crucial - without it the output is not captured in Jenkins | ||||||
| 
 | 
 | ||||||
| docker-compose run \ | CONTAINER_ID=$(docker-compose run \ | ||||||
| 			   --rm \ | 	-d \ | ||||||
| 			   --service-ports \ | 	--service-ports \ | ||||||
| 	   	   	-e "SEALIOUS_MONGO_PORT=27017" \ | 	-e "SEALIOUS_MONGO_PORT=27017" \ | ||||||
|            	-e "SEALIOUS_MONGO_HOST=db" \ | 	-e "SEALIOUS_MONGO_HOST=db" \ | ||||||
| 			   test \ | 	test \ | ||||||
| 			   npm "$@" | 	npm "$@") | ||||||
|  | 
 | ||||||
|  | docker logs -f $CONTAINER_ID | ||||||
|  | docker rm $CONTAINER_ID | ||||||
|  | |||||||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								| @ -0,0 +1,21 @@ | |||||||
|  | import { Collection, FieldTypes, Policies } from "sealious"; | ||||||
|  | 
 | ||||||
|  | export default class Tasks extends Collection { | ||||||
|  | 	fields = { | ||||||
|  | 		title: new FieldTypes.Text(), | ||||||
|  | 		done: new (class extends FieldTypes.Boolean { | ||||||
|  | 			hasDefaultValue = () => true; | ||||||
|  | 			async getDefaultValue() { | ||||||
|  | 				return false; | ||||||
|  | 			} | ||||||
|  | 		})(), | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	policies = { | ||||||
|  | 		create: new Policies.Public(), | ||||||
|  | 		show: new Policies.Owner(), | ||||||
|  | 		list: new Policies.Owner(), | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	defaultPolicy = new Policies.Public(); | ||||||
|  | } | ||||||
| @ -0,0 +1,7 @@ | |||||||
|  | const ADMIN_CREDENTIALS = { | ||||||
|  | 	username: "admin", | ||||||
|  | 	password: "adminadmin", | ||||||
|  | 	email: "admin@example.com", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default ADMIN_CREDENTIALS; | ||||||
| @ -0,0 +1,53 @@ | |||||||
|  | import { BaseContext } from "koa"; | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import frame from "../../frame"; | ||||||
|  | import { Tasks } from "../../collections/collections"; | ||||||
|  | 
 | ||||||
|  | export function Task(task: CollectionItem<typeof Tasks>) { | ||||||
|  | 	return frame( | ||||||
|  | 		`task-${task.id}`, | ||||||
|  | 		/* HTML */ `<li class="task">
 | ||||||
|  | 			<input | ||||||
|  | 				type="checkbox" | ||||||
|  | 				data-controller="task" | ||||||
|  | 				data-action="task#toggle" | ||||||
|  | 				data-id="${task.id}" | ||||||
|  | 				${task.get("done") ? "checked" : ""} | ||||||
|  | 			/> | ||||||
|  | 			${task.get("title") as string} | ||||||
|  | 			<form method="POST" action="/todo/"> | ||||||
|  | 				<input class="delete-button" type="submit" value="Delete" /> | ||||||
|  | 				<input | ||||||
|  | 					class="hidden-button" | ||||||
|  | 					type="hidden" | ||||||
|  | 					name="taskId" | ||||||
|  | 					value="${task.id}" | ||||||
|  | 				/> | ||||||
|  | 				<input | ||||||
|  | 					class="hidden-button" | ||||||
|  | 					type="hidden" | ||||||
|  | 					id="action" | ||||||
|  | 					name="action" | ||||||
|  | 					value="delete" | ||||||
|  | 				/> | ||||||
|  | 			</form> | ||||||
|  | 		</li>` | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /*  | ||||||
|  | 	<form method="DELETE" action="/todo/${task.id}"> | ||||||
|  | 				<input class="delete-button" type="submit" value="Delete" /> | ||||||
|  | 			</form> | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | export async function TaskList(ctx: BaseContext) { | ||||||
|  | 	const { items: tasks } = await ctx.$app.collections.tasks.list(ctx.$context).fetch(); | ||||||
|  | 
 | ||||||
|  | 	const tasksTemplate = tasks.map(Task).join("\n"); | ||||||
|  | 	return ` | ||||||
|  |                 <ul> | ||||||
|  |                     ${tasksTemplate} | ||||||
|  |                 </ul> | ||||||
|  |             `;
 | ||||||
|  | } | ||||||
| @ -0,0 +1,30 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { Mountable } from "@sealcode/sealgen"; | ||||||
|  | import Router from "@koa/router"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "Logout"; | ||||||
|  | 
 | ||||||
|  | export default new (class LogoutRedirect extends Mountable { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mount(router: Router, path: string) { | ||||||
|  | 		router.get(path, async (ctx) => { | ||||||
|  | 			try { | ||||||
|  | 				const session_id: string = ctx.cookies.get("sealious-session") as string; | ||||||
|  | 				if (session_id) { | ||||||
|  | 					await ctx.$app.collections.sessions.logout( | ||||||
|  | 						new ctx.$app.SuperContext(), | ||||||
|  | 						session_id | ||||||
|  | 					); | ||||||
|  | 					ctx.status = 302; | ||||||
|  | 					ctx.redirect("/"); | ||||||
|  | 				} | ||||||
|  | 			} catch (error) { | ||||||
|  | 				console.error("Error during logout:", error); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,55 @@ | |||||||
|  | import assert from "assert"; | ||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | import { LONG_TEST_TIMEOUT, VERY_LONG_TEST_TIMEOUT } from "../test_utils/webhint"; | ||||||
|  | import { LogoutURL, SignInURL } from "./urls"; | ||||||
|  | import { Browser, BrowserContext, Page } from "@playwright/test"; | ||||||
|  | import { getBrowser } from "../test_utils/browser-creator"; | ||||||
|  | import ADMIN_CREDENTIALS from "../default-admin-credentials"; | ||||||
|  | 
 | ||||||
|  | describe("Logout", () => { | ||||||
|  | 	let page: Page; | ||||||
|  | 	let browser: Browser; | ||||||
|  | 	let context: BrowserContext; | ||||||
|  | 	const username = ADMIN_CREDENTIALS.username; | ||||||
|  | 	const password = ADMIN_CREDENTIALS.password; | ||||||
|  | 
 | ||||||
|  | 	beforeEach(async () => { | ||||||
|  | 		browser = await getBrowser(); | ||||||
|  | 		context = await browser.newContext(); | ||||||
|  | 		page = await context.newPage(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	afterEach(async () => { | ||||||
|  | 		await context.close(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	it("doesn't crash", async function () { | ||||||
|  | 		this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 		return withProdApp(async ({ rest_api }) => { | ||||||
|  | 			await assert.rejects( | ||||||
|  | 				async () => { | ||||||
|  | 					await rest_api.get(LogoutURL); | ||||||
|  | 				}, | ||||||
|  | 				{ name: "Error" } | ||||||
|  | 			); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	describe("logout test", () => { | ||||||
|  | 		it("logout", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign in" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill(username); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill(password); | ||||||
|  | 				await page.getByPlaceholder("password").press("Enter"); | ||||||
|  | 				await page.waitForSelector(`a[href="${LogoutURL}"]`); | ||||||
|  | 				await page.getByRole("link", { name: "Logout" }).click(); | ||||||
|  | 				await page.waitForSelector(`a[href="${SignInURL}"]`); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
| @ -0,0 +1,121 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { | ||||||
|  | 	Form, | ||||||
|  | 	FormData, | ||||||
|  | 	FormDataValue, | ||||||
|  | 	Fields, | ||||||
|  | 	Controls, | ||||||
|  | 	FormReaction, | ||||||
|  | } from "@sealcode/sealgen"; | ||||||
|  | import html from "../html"; | ||||||
|  | import { Users } from "../collections/collections"; | ||||||
|  | import { FlatTemplatable, tempstream } from "tempstream"; | ||||||
|  | import { PageErrorMessage } from "@sealcode/sealgen/@types/page/mountable-with-fields"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "SignIn"; | ||||||
|  | 
 | ||||||
|  | const fields = { | ||||||
|  | 	username: new Fields.SimpleFormField(true), | ||||||
|  | 	password: new Fields.SimpleFormField(true), | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const SignInShape = Fields.fieldsToShape(fields); | ||||||
|  | 
 | ||||||
|  | export default new (class SignInForm extends Form<typeof fields, void> { | ||||||
|  | 	defaultSuccessMessage = "Formularz wypełniony poprawnie"; | ||||||
|  | 	fields = fields; | ||||||
|  | 
 | ||||||
|  | 	controls = [ | ||||||
|  | 		new Controls.SimpleInput(fields.username, { label: "Username:", type: "text" }), | ||||||
|  | 		new Controls.SimpleInput(fields.password, { | ||||||
|  | 			label: "Password:", | ||||||
|  | 			type: "password", | ||||||
|  | 		}), | ||||||
|  | 	]; | ||||||
|  | 
 | ||||||
|  | 	async validateValues( | ||||||
|  | 		ctx: Context, | ||||||
|  | 		data: Record<string, FormDataValue> | ||||||
|  | 	): Promise<{ valid: boolean; error: string }> { | ||||||
|  | 		const { parsed: username } = await this.fields.username.getValue(ctx, data); | ||||||
|  | 
 | ||||||
|  | 		const filter: object = typeof username === "string" ? { username } : {}; | ||||||
|  | 
 | ||||||
|  | 		const user = await Users.suList().filter(filter).fetch(); | ||||||
|  | 
 | ||||||
|  | 		if (user.empty) { | ||||||
|  | 			return { valid: false, error: `Incorrect password or username` }; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return { valid: true, error: `` }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(ctx: Context) { | ||||||
|  | 		if (ctx.$context.session_id) { | ||||||
|  | 			return { canAccess: false, message: "" }; | ||||||
|  | 		} | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async onSuccess( | ||||||
|  | 		_: Context, | ||||||
|  | 		__: FormData<string>, | ||||||
|  | 		_submitResult: void | ||||||
|  | 	): Promise<FormReaction> { | ||||||
|  | 		const reaction: FormReaction = { | ||||||
|  | 			action: "redirect", | ||||||
|  | 			url: "/", | ||||||
|  | 		}; | ||||||
|  | 		console.log("Successfully logged in."); | ||||||
|  | 		return reaction; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async onError( | ||||||
|  | 		ctx: Context, | ||||||
|  | 		data: FormData<string>, | ||||||
|  | 		error: unknown | ||||||
|  | 	): Promise<FormReaction> { | ||||||
|  | 		const reaction: FormReaction = { | ||||||
|  | 			action: "stay", | ||||||
|  | 			content: this.render(ctx, data, true), | ||||||
|  | 			messages: [ | ||||||
|  | 				{ | ||||||
|  | 					type: "error", | ||||||
|  | 					text: `There was an error while logging in: ${String(error)}`, | ||||||
|  | 				}, | ||||||
|  | 			], | ||||||
|  | 		}; | ||||||
|  | 		return reaction; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async onSubmit(ctx: Context, data: FormData) { | ||||||
|  | 		try { | ||||||
|  | 			const sessionId: string = await Users.app.collections.sessions.login( | ||||||
|  | 				data.raw_values.username as string, | ||||||
|  | 				data.raw_values.password as string | ||||||
|  | 			); | ||||||
|  | 
 | ||||||
|  | 			ctx.cookies.set("sealious-session", sessionId, { | ||||||
|  | 				maxAge: 1000 * 60 * 60 * 24 * 7, | ||||||
|  | 				secure: ctx.request.protocol === "https", | ||||||
|  | 				overwrite: true, | ||||||
|  | 			}); | ||||||
|  | 		} catch (error) { | ||||||
|  | 			throw new Error(String(error)); | ||||||
|  | 		} | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async renderError(ctx: Context, error: PageErrorMessage): Promise<FlatTemplatable> { | ||||||
|  | 		return html(ctx, "SignIn", `${error.message}`); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context, data: FormData, show_field_errors: boolean) { | ||||||
|  | 		return html( | ||||||
|  | 			ctx, | ||||||
|  | 			"SignIn", | ||||||
|  | 			tempstream`${await super.render(ctx, data, show_field_errors)}` | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,103 @@ | |||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint"; | ||||||
|  | import { SignInURL, LogoutURL } from "./urls"; | ||||||
|  | import { Browser, BrowserContext, Page } from "@playwright/test"; | ||||||
|  | import { getBrowser } from "../test_utils/browser-creator"; | ||||||
|  | import ADMIN_CREDENTIALS from "../default-admin-credentials"; | ||||||
|  | 
 | ||||||
|  | describe("SignIn", () => { | ||||||
|  | 	let page: Page; | ||||||
|  | 	let browser: Browser; | ||||||
|  | 	let context: BrowserContext; | ||||||
|  | 	const username = ADMIN_CREDENTIALS.username; | ||||||
|  | 	const password = ADMIN_CREDENTIALS.password; | ||||||
|  | 
 | ||||||
|  | 	beforeEach(async () => { | ||||||
|  | 		browser = await getBrowser(); | ||||||
|  | 		context = await browser.newContext(); | ||||||
|  | 		page = await context.newPage(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	afterEach(async () => { | ||||||
|  | 		await context.close(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	it("doesn't crash", async function () { | ||||||
|  | 		this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 		return withProdApp(async ({ base_url, rest_api }) => { | ||||||
|  | 			await rest_api.get(SignInURL); | ||||||
|  | 			await webhintURL(base_url + SignInURL); | ||||||
|  | 			// alternatively you can use webhintHTML for faster but less precise scans
 | ||||||
|  | 			// or for scanning responses of requests that use some form of authorization:
 | ||||||
|  | 			// const response = await rest_api.get(SignInURL);
 | ||||||
|  | 			// await webhintHTML(response);
 | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	describe("can access test", () => { | ||||||
|  | 		it("access url", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign in" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill(username); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill(password); | ||||||
|  | 				await page.getByPlaceholder("password").press("Enter"); | ||||||
|  | 				await page.waitForSelector(`a[href="${LogoutURL}"]`); | ||||||
|  | 				await page.goto(base_url + SignInURL); | ||||||
|  | 				await page.waitForSelector('body:has-text("no access")'); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Logout" }).click(); | ||||||
|  | 				await page.waitForSelector(`a[href="${SignInURL}"]`); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	describe("sign in test", () => { | ||||||
|  | 		it("wrong username", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign in" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill("username20230720722"); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill("test"); | ||||||
|  | 				await page.getByPlaceholder("password").press("Enter"); | ||||||
|  | 				await page.waitForSelector(".form-message"); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		it("correct username and password", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign in" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill(username); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill(password); | ||||||
|  | 				await page.getByPlaceholder("password").press("Enter"); | ||||||
|  | 				await page.waitForSelector(`a[href="${LogoutURL}"]`); | ||||||
|  | 				await page.getByRole("link", { name: "Logout" }).click(); | ||||||
|  | 				await page.waitForSelector(`a[href="${SignInURL}"]`); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		it("wrong password", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign in" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill(username); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill("asddasads20230720722"); | ||||||
|  | 				await page.getByPlaceholder("password").press("Enter"); | ||||||
|  | 				await page.waitForSelector(".form-message"); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
| @ -0,0 +1,139 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { | ||||||
|  | 	Form, | ||||||
|  | 	FormData, | ||||||
|  | 	FormDataValue, | ||||||
|  | 	Fields, | ||||||
|  | 	Controls, | ||||||
|  | 	FormReaction, | ||||||
|  | } from "@sealcode/sealgen"; | ||||||
|  | import html from "../html"; | ||||||
|  | import { Users } from "../collections/collections"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "SignUp"; | ||||||
|  | 
 | ||||||
|  | const fields = { | ||||||
|  | 	username: new Fields.CollectionField(true, Users.fields.username), | ||||||
|  | 	email: new Fields.CollectionField(true, Users.fields.email), | ||||||
|  | 	password: new Fields.SimpleFormField(true), | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const SignUpShape = Fields.fieldsToShape(fields); | ||||||
|  | 
 | ||||||
|  | export default new (class SignUpForm extends Form<typeof fields, void> { | ||||||
|  | 	defaultSuccessMessage = "Formularz wypełniony poprawnie"; | ||||||
|  | 	fields = fields; | ||||||
|  | 
 | ||||||
|  | 	controls = [ | ||||||
|  | 		new Controls.SimpleInput(fields.username, { label: "Username:", type: "text" }), | ||||||
|  | 		new Controls.SimpleInput(fields.email, { label: "Email:", type: "email" }), | ||||||
|  | 		new Controls.SimpleInput(fields.password, { | ||||||
|  | 			label: "Password:", | ||||||
|  | 			type: "password", | ||||||
|  | 		}), | ||||||
|  | 	]; | ||||||
|  | 
 | ||||||
|  | 	async validateValues( | ||||||
|  | 		ctx: Context, | ||||||
|  | 		data: Record<string, FormDataValue> | ||||||
|  | 	): Promise<{ valid: boolean; error: string }> { | ||||||
|  | 		const { parsed: email } = await this.fields.email.getValue(ctx, data); | ||||||
|  | 		const { parsed: password } = await this.fields.password.getValue(ctx, data); | ||||||
|  | 
 | ||||||
|  | 		if ((password || "").length >= 8) { | ||||||
|  | 			const user = await Users.suList().filter({ email: email }).fetch(); | ||||||
|  | 			if (user.empty) { | ||||||
|  | 				return { valid: true, error: `` }; | ||||||
|  | 			} | ||||||
|  | 			return { valid: false, error: `Email is arleady taken` }; | ||||||
|  | 		} else { | ||||||
|  | 			return { | ||||||
|  | 				valid: false, | ||||||
|  | 				error: "Password must contain a minimum of 8 characters", | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(ctx: Context) { | ||||||
|  | 		if (ctx.$context.session_id) { | ||||||
|  | 			return { canAccess: false, message: "" }; | ||||||
|  | 		} | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async onError( | ||||||
|  | 		ctx: Context, | ||||||
|  | 		data: FormData<string>, | ||||||
|  | 		error: unknown | ||||||
|  | 	): Promise<FormReaction> { | ||||||
|  | 		const reaction: FormReaction = { | ||||||
|  | 			action: "stay", | ||||||
|  | 			content: this.render(ctx, data, true), | ||||||
|  | 			messages: [ | ||||||
|  | 				{ | ||||||
|  | 					type: "error", | ||||||
|  | 					text: `An unexpected error occurred, try again. <br> Error${ | ||||||
|  | 						error as string | ||||||
|  | 					}`,
 | ||||||
|  | 				}, | ||||||
|  | 			], | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		return reaction; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async onSuccess(ctx: Context, data: FormData): Promise<FormReaction> { | ||||||
|  | 		const username: FormDataValue = data.raw_values.username; | ||||||
|  | 		const reaction: FormReaction = { | ||||||
|  | 			action: "stay", | ||||||
|  | 			content: `Hello ${String( | ||||||
|  | 				username | ||||||
|  | 			)}. <p class="success-notify">Your account has been successfully created.</p> | ||||||
|  | 					<a href="/" class="nav-logo"> | ||||||
|  | 						<img | ||||||
|  | 							src="/assets/logo" | ||||||
|  | 							alt="${ctx.$app.manifest.name} - logo" | ||||||
|  | 							width="50" | ||||||
|  | 							height="50" | ||||||
|  | 						/> | ||||||
|  | 						Sealious App | ||||||
|  | 					</a>`,
 | ||||||
|  | 			messages: [ | ||||||
|  | 				{ | ||||||
|  | 					type: "success", | ||||||
|  | 					text: "", | ||||||
|  | 				}, | ||||||
|  | 			], | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		return reaction; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async onSubmit(ctx: Context, data: FormData) { | ||||||
|  | 		const username: string = | ||||||
|  | 			typeof data.raw_values.username === "string" ? data.raw_values.username : ""; | ||||||
|  | 		const password: string = | ||||||
|  | 			typeof data.raw_values.password === "string" ? data.raw_values.password : ""; | ||||||
|  | 		const email: string = | ||||||
|  | 			typeof data.raw_values.email === "string" ? data.raw_values.email : ""; | ||||||
|  | 
 | ||||||
|  | 		try { | ||||||
|  | 			await Users.suCreate({ | ||||||
|  | 				username: username, | ||||||
|  | 				password: password, | ||||||
|  | 				email: email, | ||||||
|  | 				roles: [], | ||||||
|  | 			}); | ||||||
|  | 			console.log("A user was created successfully."); | ||||||
|  | 		} catch (error) { | ||||||
|  | 			console.error("Error during user creation:", error); | ||||||
|  | 			throw new Error(String(error)); | ||||||
|  | 		} | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context, data: FormData, show_field_errors: boolean) { | ||||||
|  | 		return html(ctx, "SignUp", await super.render(ctx, data, show_field_errors)); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,122 @@ | |||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint"; | ||||||
|  | import { SignUpURL, LogoutURL, SignInURL } from "./urls"; | ||||||
|  | import { Browser, BrowserContext, Page } from "@playwright/test"; | ||||||
|  | import { getBrowser } from "../test_utils/browser-creator"; | ||||||
|  | import ADMIN_CREDENTIALS from "../default-admin-credentials"; | ||||||
|  | 
 | ||||||
|  | describe("SignUp", () => { | ||||||
|  | 	let page: Page; | ||||||
|  | 	let browser: Browser; | ||||||
|  | 	let context: BrowserContext; | ||||||
|  | 	const username = ADMIN_CREDENTIALS.username; | ||||||
|  | 	const password = ADMIN_CREDENTIALS.password; | ||||||
|  | 	const email = ADMIN_CREDENTIALS.email; | ||||||
|  | 
 | ||||||
|  | 	beforeEach(async () => { | ||||||
|  | 		browser = await getBrowser(); | ||||||
|  | 		context = await browser.newContext(); | ||||||
|  | 		page = await context.newPage(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	afterEach(async () => { | ||||||
|  | 		await context.close(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	it("doesn't crash", async function () { | ||||||
|  | 		this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 		return withProdApp(async ({ base_url, rest_api }) => { | ||||||
|  | 			await rest_api.get(SignUpURL); | ||||||
|  | 			await webhintURL(base_url + SignUpURL); | ||||||
|  | 			// alternatively you can use webhintHTML for faster but less precise scans
 | ||||||
|  | 			// or for scanning responses of requests that use some form of authorization:
 | ||||||
|  | 			// const response = await rest_api.get(SignUpURL);
 | ||||||
|  | 			// await webhintHTML(response);
 | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	describe("signup test", () => { | ||||||
|  | 		it("username is taken", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign up" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill(username); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("email").fill("user0192939@randomsuper.com"); | ||||||
|  | 				await page.getByPlaceholder("email").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill("user12341234"); | ||||||
|  | 				await page.getByRole("button", { name: "Wyślij" }).click(); | ||||||
|  | 				await page.waitForSelector(".input__error"); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 		it("password is too shot ", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign up" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill("dasdsa"); | ||||||
|  | 				await page.getByPlaceholder("email").click(); | ||||||
|  | 				await page | ||||||
|  | 					.getByPlaceholder("email") | ||||||
|  | 					.fill("asasdsdadsadss123asddsa@asdasca.com"); | ||||||
|  | 				await page.getByPlaceholder("password").click(); | ||||||
|  | 				await page.getByPlaceholder("password").fill("asddsa"); | ||||||
|  | 				await page.getByRole("button", { name: "Wyślij" }).click(); | ||||||
|  | 				await page.waitForSelector(".form-message.form-message--error"); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 		it("email is taken", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign up" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill("ranomusername2023072722"); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("email").fill(email); | ||||||
|  | 				await page.getByPlaceholder("email").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill("asdasdasdasdasd"); | ||||||
|  | 				await page.getByRole("button", { name: "Wyślij" }).click(); | ||||||
|  | 				await page.waitForSelector(".form-message.form-message--error"); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 		it("correct", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign up" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill("ranomusername20230720722"); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("email").fill("radomemail@emailrandom.com"); | ||||||
|  | 				await page.getByPlaceholder("email").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill("asdasdasdasdasd"); | ||||||
|  | 				await page.getByRole("button", { name: "Wyślij" }).click(); | ||||||
|  | 				await page.waitForSelector(".success-notify"); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 	describe("can access test", () => { | ||||||
|  | 		it("access url", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign in" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill(username); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill(password); | ||||||
|  | 				await page.getByPlaceholder("password").press("Enter"); | ||||||
|  | 				await page.waitForSelector(`a[href="${LogoutURL}"]`); | ||||||
|  | 				await page.goto(base_url + SignUpURL); | ||||||
|  | 				await page.waitForSelector('body:has-text("no access")'); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Logout" }).click(); | ||||||
|  | 				await page.waitForSelector(`a[href="${SignInURL}"]`); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
| @ -0,0 +1,107 @@ | |||||||
|  | import { Tasks } from "./../collections/collections"; | ||||||
|  | import { tempstream } from "tempstream"; | ||||||
|  | import { Context } from "koa"; | ||||||
|  | import { Form, FormData, FormDataValue, Fields, Controls } from "@sealcode/sealgen"; | ||||||
|  | import html from "../html"; | ||||||
|  | import { TaskList } from "./common/tasks-view"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "Todo"; | ||||||
|  | 
 | ||||||
|  | const fields = { | ||||||
|  | 	name: new Fields.CollectionField(true, Tasks.fields.title), | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const TodoShape = Fields.fieldsToShape(fields); | ||||||
|  | 
 | ||||||
|  | export default new (class TodoForm extends Form<typeof fields, void> { | ||||||
|  | 	defaultSuccessMessage = "Task has been successfully created"; | ||||||
|  | 	fields = fields; | ||||||
|  | 
 | ||||||
|  | 	controls = [ | ||||||
|  | 		new Controls.SimpleInput(fields.name, { | ||||||
|  | 			label: "Task name:", | ||||||
|  | 			type: "text", | ||||||
|  | 			placeholder: "Write an Matrix bot", | ||||||
|  | 		}), | ||||||
|  | 		new Controls.HTML("decoration", (fctx) => { | ||||||
|  | 			return `<input class="hidden-button" type="hidden" id="action" name="action" value="create" form="${fctx.form_id}" />`; | ||||||
|  | 		}), | ||||||
|  | 	]; | ||||||
|  | 
 | ||||||
|  | 	async validateValues( | ||||||
|  | 		ctx: Context, | ||||||
|  | 		data: Record<string, FormDataValue> | ||||||
|  | 	): Promise<{ valid: boolean; error: string }> { | ||||||
|  | 		const { parsed: name } = await this.fields.name.getValue(ctx, data); | ||||||
|  | 
 | ||||||
|  | 		if ((name || "").length < 3) { | ||||||
|  | 			return { | ||||||
|  | 				valid: true, | ||||||
|  | 				error: "The name of the task must have at least 3 characters", | ||||||
|  | 			}; | ||||||
|  | 		} else { | ||||||
|  | 			const filter: object = name ? { title: name } : {}; | ||||||
|  | 
 | ||||||
|  | 			const tasks = await ctx.$app.collections.tasks | ||||||
|  | 				.list(ctx.$context) | ||||||
|  | 				.filter(filter) | ||||||
|  | 				.fetch(); | ||||||
|  | 			if (tasks.empty) { | ||||||
|  | 				return { valid: true, error: "" }; | ||||||
|  | 			} | ||||||
|  | 			return { valid: false, error: "Task with the same name already exists" }; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(ctx: Context) { | ||||||
|  | 		if (ctx.$context.session_id) { | ||||||
|  | 			return { canAccess: true, message: "" }; | ||||||
|  | 		} | ||||||
|  | 		return { canAccess: false, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async onSubmit(ctx: Context, data: FormData) { | ||||||
|  | 		const action: FormDataValue = data.raw_values.action; | ||||||
|  | 
 | ||||||
|  | 		switch (action) { | ||||||
|  | 			case "create": { | ||||||
|  | 				try { | ||||||
|  | 					await ctx.$app.collections.tasks.create(ctx.$context, { | ||||||
|  | 						title: String(data.raw_values.name), | ||||||
|  | 						done: false, | ||||||
|  | 					}); | ||||||
|  | 				} catch (error) { | ||||||
|  | 					throw new Error(); | ||||||
|  | 				} | ||||||
|  | 				console.debug(`task has been successfully created`); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 			case "delete": { | ||||||
|  | 				const task = await ctx.$app.collections.tasks.getByID( | ||||||
|  | 					ctx.$context, | ||||||
|  | 					data.raw_values.taskId as string | ||||||
|  | 				); | ||||||
|  | 				await task.remove(ctx.$context); | ||||||
|  | 				console.debug(`task has been successfully removed`); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 			default: { | ||||||
|  | 				console.debug("Wrong action"); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context, data: FormData, show_field_errors: boolean) { | ||||||
|  | 		return html( | ||||||
|  | 			ctx, | ||||||
|  | 			"Todo", | ||||||
|  | 			tempstream`${await super.render(ctx, data, show_field_errors)} | ||||||
|  | 					${TaskList(ctx)} | ||||||
|  | 					` | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,81 @@ | |||||||
|  | import assert from "assert"; | ||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | import { LONG_TEST_TIMEOUT, VERY_LONG_TEST_TIMEOUT } from "../test_utils/webhint"; | ||||||
|  | import { SignInURL, TodoURL } from "./urls"; | ||||||
|  | import { Browser, BrowserContext, Page } from "@playwright/test"; | ||||||
|  | import { getBrowser } from "../test_utils/browser-creator"; | ||||||
|  | import ADMIN_CREDENTIALS from "../default-admin-credentials"; | ||||||
|  | 
 | ||||||
|  | describe("Todo", function () { | ||||||
|  | 	let page: Page; | ||||||
|  | 	let browser: Browser; | ||||||
|  | 	let context: BrowserContext; | ||||||
|  | 	const username = ADMIN_CREDENTIALS.username; | ||||||
|  | 	const password = ADMIN_CREDENTIALS.password; | ||||||
|  | 
 | ||||||
|  | 	beforeEach(async () => { | ||||||
|  | 		browser = await getBrowser(); | ||||||
|  | 		context = await browser.newContext(); | ||||||
|  | 		page = await context.newPage(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	afterEach(async () => { | ||||||
|  | 		await context.close(); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	it("doesn't crash", async function () { | ||||||
|  | 		this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 		return withProdApp(async ({ rest_api }) => { | ||||||
|  | 			await assert.rejects( | ||||||
|  | 				async () => { | ||||||
|  | 					await rest_api.get(TodoURL); | ||||||
|  | 				}, | ||||||
|  | 				{ name: "Error" } | ||||||
|  | 			); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	describe("todo test", () => { | ||||||
|  | 		it("create and delete task", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(VERY_LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				await page.getByRole("link", { name: "Sign in" }).click(); | ||||||
|  | 				await page.getByPlaceholder("text").click(); | ||||||
|  | 				await page.getByPlaceholder("text").fill(username); | ||||||
|  | 				await page.getByPlaceholder("text").press("Tab"); | ||||||
|  | 				await page.getByPlaceholder("password").fill(password); | ||||||
|  | 				await page.getByPlaceholder("password").press("Enter"); | ||||||
|  | 				await page.getByRole("link", { name: "To do app" }).click(); | ||||||
|  | 				await page.getByPlaceholder("Write an Matrix bot").click(); | ||||||
|  | 				await page.getByPlaceholder("Write an Matrix bot").fill("randomtasdk"); | ||||||
|  | 				await page.getByRole("button", { name: "Wyślij" }).click(); | ||||||
|  | 				await page.waitForSelector(".form-message.form-message--success"); | ||||||
|  | 				await page.locator("turbo-frame").getByRole("checkbox").check(); | ||||||
|  | 				await page.locator("turbo-frame").getByRole("checkbox").uncheck(); | ||||||
|  | 				await page | ||||||
|  | 					.locator("turbo-frame") | ||||||
|  | 					.getByRole("button", { name: "Delete" }) | ||||||
|  | 					.click(); | ||||||
|  | 				await page.getByRole("link", { name: "Logout" }).click(); | ||||||
|  | 				await page.waitForSelector(`a[href="${SignInURL}"]`); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	describe("can access test", () => { | ||||||
|  | 		it("access url", async function () { | ||||||
|  | 			await withProdApp(async ({ base_url }) => { | ||||||
|  | 				this.timeout(LONG_TEST_TIMEOUT); | ||||||
|  | 				await page.goto(base_url); | ||||||
|  | 				try { | ||||||
|  | 					await page.waitForSelector(`a[href="${SignInURL}"]`); | ||||||
|  | 					await page.goto(base_url + TodoURL); | ||||||
|  | 					await page.waitForSelector('body:has-text("no access")'); | ||||||
|  | 				} catch (error) { | ||||||
|  | 					console.error(error); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
| @ -1 +1,5 @@ | |||||||
| export const HelloURL = "/hello/"; | export const HelloURL = "/hello/"; | ||||||
|  | export const LogoutURL = "/logout/"; | ||||||
|  | export const SignInURL = "/signIn/"; | ||||||
|  | export const SignUpURL = "/signUp/"; | ||||||
|  | export const TodoURL = "/todo/"; | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| describe("sample test", () => { | describe("sample test", () => { | ||||||
| 	it("always passes", () => { | 	it("always passes", () => { | ||||||
| 		return true; | 		return; | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -0,0 +1,16 @@ | |||||||
|  | import { Browser, firefox } from "@playwright/test"; | ||||||
|  | 
 | ||||||
|  | let browser: Browser; | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||||
|  | export async function getBrowser(): Promise<Browser> { | ||||||
|  | 	if (!browser) browser = await firefox.launch(); | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unsafe-return
 | ||||||
|  | 	return browser; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function closeBrowser() { | ||||||
|  | 	if (browser) { | ||||||
|  | 		await browser.close(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,30 @@ | |||||||
|  | import { Controller } from "stimulus"; | ||||||
|  | 
 | ||||||
|  | export default class TaskController extends Controller { | ||||||
|  | 	id: string; | ||||||
|  | 
 | ||||||
|  | 	connect() { | ||||||
|  | 		const dataIdAttr = this.element.getAttribute("data-id"); | ||||||
|  | 		if (dataIdAttr) { | ||||||
|  | 			this.id = dataIdAttr; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async toggle(event: Event) { | ||||||
|  | 		const inputElement: HTMLInputElement = event.target as HTMLInputElement; | ||||||
|  | 
 | ||||||
|  | 		if (inputElement instanceof HTMLInputElement) { | ||||||
|  | 			const isChecked: boolean = inputElement.checked; | ||||||
|  | 
 | ||||||
|  | 			await fetch(`/api/v1/collections/tasks/${this.id}`, { | ||||||
|  | 				method: "PATCH", | ||||||
|  | 				headers: { | ||||||
|  | 					"Content-Type": "application/json", | ||||||
|  | 				}, | ||||||
|  | 				body: JSON.stringify({ | ||||||
|  | 					done: isChecked, | ||||||
|  | 				}), | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -1,6 +1,8 @@ | |||||||
| import * as Turbo from "@hotwired/turbo"; | import * as Turbo from "@hotwired/turbo"; | ||||||
| import { Application } from "stimulus"; | import { Application } from "stimulus"; | ||||||
|  | import TaskController from "./controllers/task-controller"; | ||||||
| 
 | 
 | ||||||
| export { Turbo }; | export { Turbo }; | ||||||
| 
 | 
 | ||||||
| const application = Application.start(); | const application = Application.start(); | ||||||
|  | application.register("task", TaskController); | ||||||
|  | |||||||
					Loading…
					
					
				
		Reference in New Issue