Latest type and dep improvements
1
.gitignore
vendored
@ -45,3 +45,4 @@ src/back/routes/urls.ts
|
|||||||
src/includes.css
|
src/includes.css
|
||||||
.eslint-result.json
|
.eslint-result.json
|
||||||
lint-report.json
|
lint-report.json
|
||||||
|
/src/style-entrypoints/
|
||||||
|
14
README.md
@ -16,7 +16,7 @@ npm install
|
|||||||
|
|
||||||
```
|
```
|
||||||
npx sealgen make-env
|
npx sealgen make-env
|
||||||
docker-compose up -d db mailcatcher
|
docker-compose up -d db mailcatcher meilisearch
|
||||||
npm run watch
|
npm run watch
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -41,3 +41,15 @@ And then
|
|||||||
```
|
```
|
||||||
npm run test
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The tests don't run webhint by default - it's delegated to run on Jenkins.
|
||||||
|
|
||||||
|
If Jenkins complains about a webhint issue and you'd like to reproduce that on your local machine, run
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run full-test
|
||||||
|
```
|
||||||
|
|
||||||
|
The tests run in pararell mode by default, to speed things up. Because of that, the
|
||||||
|
`.only` syntax doesn't work. To run tests without pararell mode, run `npm run full-test
|
||||||
|
--serial` - you can then use the `.only` syntax to run a particular test
|
||||||
|
@ -3,7 +3,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: mongo:4.4-bionic
|
image: mongo:4.4-bionic
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${PORT:-2074}7:27017"
|
- "127.0.0.1:${PORT:-2076}7:27017"
|
||||||
test:
|
test:
|
||||||
image: sealious-app:latest
|
image: sealious-app:latest
|
||||||
build:
|
build:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:18-bullseye-slim
|
FROM node:21-bullseye-slim
|
||||||
|
|
||||||
ENV HOME=/opt/sealious-app
|
ENV HOME=/opt/sealious-app
|
||||||
|
|
||||||
@ -11,7 +11,9 @@ RUN apt install -y git
|
|||||||
RUN apt install -y tmux
|
RUN apt install -y tmux
|
||||||
|
|
||||||
# playwright deps
|
# playwright deps
|
||||||
RUN apt-get update&& apt-get install -y --no-install-recommends libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 xvfb fonts-noto-color-emoji fonts-unifont libfontconfig1 libfreetype6 xfonts-cyrillic xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf libcairo-gobject2 libdbus-glib-1-2 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu67 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libsoup2.4-1 libwayland-egl1 libwayland-server0 libwebp6 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7
|
RUN apt-get update&& apt-get install -y --no-install-recommends libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 xvfb fonts-noto-color-emoji fonts-unifont libfontconfig1 libfreetype6 xfonts-cyrillic xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf libcairo-gobject2 libdbus-glib-1-2 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu67 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libsoup2.4-1 libwayland-egl1 libwayland-server0 libwebp6 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7 python
|
||||||
|
|
||||||
|
RUN apt install wget
|
||||||
|
|
||||||
RUN chmod +x /tini
|
RUN chmod +x /tini
|
||||||
ENTRYPOINT ["/tini", "--"]
|
ENTRYPOINT ["/tini", "--"]
|
||||||
|
@ -19,6 +19,5 @@ mkdir -p docker_node_modules
|
|||||||
docker compose build
|
docker compose build
|
||||||
|
|
||||||
docker compose up -d db
|
docker compose up -d db
|
||||||
./npm.sh --no-TTY ci && ./npm.sh --no-TTY run build
|
|
||||||
|
|
||||||
rm -f log.html
|
rm -f log.html
|
||||||
|
@ -17,11 +17,14 @@ export SEALIOUS_BASE_URL
|
|||||||
node -e 'console.log(JSON.stringify(JSON.parse(fs.readFileSync("./.eslint-result.json")).map(fileResult => fileResult.messages.map(message=>({...message, path: fileResult.filePath.replace("/opt/sealious-app/", "")}))).flat().map(message=>({name: message.ruleId, code: message.ruleId, severity: message.severity==1 ? "warning" : "error", path: message.path, line: message.line, char: message.column, description: message.message}))))' \
|
node -e 'console.log(JSON.stringify(JSON.parse(fs.readFileSync("./.eslint-result.json")).map(fileResult => fileResult.messages.map(message=>({...message, path: fileResult.filePath.replace("/opt/sealious-app/", "")}))).flat().map(message=>({name: message.ruleId, code: message.ruleId, severity: message.severity==1 ? "warning" : "error", path: message.path, line: message.line, char: message.column, description: message.message}))))' \
|
||||||
>lint-report.json
|
>lint-report.json
|
||||||
|
|
||||||
|
docker-compose up -d meilisearch mailcatcher
|
||||||
|
|
||||||
CONTAINER_ID=$(docker compose run -d --user="$UID" \
|
CONTAINER_ID=$(docker compose run -d --user="$UID" \
|
||||||
-e "SEALIOUS_MONGO_PORT=27017" \
|
-e "SEALIOUS_MONGO_PORT=27017" \
|
||||||
-e "SEALIOUS_MONGO_HOST=db" \
|
-e "SEALIOUS_MONGO_HOST=db" \
|
||||||
-e "SEALIOUS_PORT=8080" \
|
-e "SEALIOUS_PORT=8080" \
|
||||||
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
||||||
|
-e "MEILISEARCH_HOST=http://meilisearch:7700" \
|
||||||
-e "SEALIOUS_MAILCATCHER_HOST=mailcatcher" \
|
-e "SEALIOUS_MAILCATCHER_HOST=mailcatcher" \
|
||||||
-e "SEALIOUS_MAILCATCHER_API_PORT=1080" \
|
-e "SEALIOUS_MAILCATCHER_API_PORT=1080" \
|
||||||
-e "SEALIOUS_MAILCATCHER_SMTP_PORT=1025" \
|
-e "SEALIOUS_MAILCATCHER_SMTP_PORT=1025" \
|
||||||
|
@ -4,9 +4,12 @@ SEALIOUS_PORT="${PORT}0"
|
|||||||
SEALIOUS_BASE_URL=$(cat .base_url)
|
SEALIOUS_BASE_URL=$(cat .base_url)
|
||||||
export SEALIOUS_BASE_URL
|
export SEALIOUS_BASE_URL
|
||||||
|
|
||||||
|
./npm.sh --no-TTY ci
|
||||||
|
|
||||||
./npm.sh --no-TTY --user="$UID" run build:front
|
./npm.sh --no-TTY --user="$UID" run build:front
|
||||||
|
|
||||||
docker compose up -d mailcatcher
|
docker compose up -d mailcatcher
|
||||||
|
docker compose up -d meilisearch
|
||||||
|
|
||||||
docker compose run --user="$UID" \
|
docker compose run --user="$UID" \
|
||||||
-e "SEALIOUS_MONGO_PORT=27017" \
|
-e "SEALIOUS_MONGO_PORT=27017" \
|
||||||
@ -14,6 +17,7 @@ docker compose run --user="$UID" \
|
|||||||
-e "SEALIOUS_PORT=$SEALIOUS_PORT" \
|
-e "SEALIOUS_PORT=$SEALIOUS_PORT" \
|
||||||
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
|
||||||
-e "SEALIOUS_MAILER=mailcatcher" \
|
-e "SEALIOUS_MAILER=mailcatcher" \
|
||||||
|
-e "MEILISEARCH_HOST=http://meilisearch:7700" \
|
||||||
-p "${SEALIOUS_PORT}:${SEALIOUS_PORT}" \
|
-p "${SEALIOUS_PORT}:${SEALIOUS_PORT}" \
|
||||||
-d \
|
-d \
|
||||||
test \
|
test \
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"delay": "500"
|
"delay": "200",
|
||||||
|
"ext": "js",
|
||||||
|
"ignore": ["public"]
|
||||||
}
|
}
|
||||||
|
5601
package-lock.json
generated
58
package.json
@ -37,31 +37,72 @@
|
|||||||
"npm run start-watch"
|
"npm run start-watch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"sealgen": {
|
||||||
|
"styleDirs": [
|
||||||
|
"node_modules/@sealcode/jdd-editor/assets"
|
||||||
|
],
|
||||||
|
"controllerDirs": [
|
||||||
|
"node_modules/@sealcode/jdd-editor/src/controllers"
|
||||||
|
],
|
||||||
|
"copyToPublic": [
|
||||||
|
{
|
||||||
|
"from": "node_modules/@sealcode/jdd-editor/assets/icons",
|
||||||
|
"to": "dist/jdd-page/icons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "node_modules/@sealcode/simplemde/dist/simplemde.min.css",
|
||||||
|
"to": "dist/simplemde.min.css"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "node_modules/@sealcode/simplemde/dist/simplemde.min.js",
|
||||||
|
"to": "dist/simplemde.min.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "node_modules/@sealcode/sealcodemirror/lib/codemirror.js",
|
||||||
|
"to": "dist/codemirror.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "node_modules/@sealcode/sealcodemirror/lib/codemirror.css",
|
||||||
|
"to": "dist/codemirror.css"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "node_modules/@sealcode/sealcodemirror/mode/javascript/javascript.js",
|
||||||
|
"to": "dist/codemirror-javascript-mode.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@hotwired/turbo": "^8.0.2",
|
"@hotwired/turbo": "^8.0.2",
|
||||||
"@koa/router": "^12.0.1",
|
"@koa/router": "^12.0.1",
|
||||||
|
"@sealcode/crud-ui": "^0.0.17",
|
||||||
"@sealcode/file-manager": "^1.0.2",
|
"@sealcode/file-manager": "^1.0.2",
|
||||||
"@sealcode/jdd": "^0.5.1",
|
"@sealcode/jdd": "^0.6.5",
|
||||||
"@sealcode/sealgen": "^0.15.47",
|
"@sealcode/jdd-editor": "^0.1.5",
|
||||||
|
"@sealcode/sealgen": "^0.17.5",
|
||||||
|
"@sealcode/simplemde": "^1.12.0-beta12",
|
||||||
"@sealcode/ts-predicates": "^0.6.2",
|
"@sealcode/ts-predicates": "^0.6.2",
|
||||||
"@types/kill-port": "^2.0.0",
|
"@types/kill-port": "^2.0.0",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/turndown": "^5.0.4",
|
"@types/object-hash": "^3.0.6",
|
||||||
|
"@types/simplemde": "^1.11.11",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"escape-goat": "^4.0.0",
|
"escape-goat": "^4.0.0",
|
||||||
"get-port": "^7.0.0",
|
"get-port": "^7.0.0",
|
||||||
"js-convert-case": "^4.2.0",
|
"js-convert-case": "^4.2.0",
|
||||||
"koa-responsive-image-router": "^0.2.29",
|
"koa-responsive-image-router": "^0.2.37",
|
||||||
"locreq": "^3.0.0",
|
"locreq": "^3.0.0",
|
||||||
|
"meilisearch": "^0.41.0",
|
||||||
"multiple-scripts-tmux": "^1.0.4",
|
"multiple-scripts-tmux": "^1.0.4",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"object-path": "^0.11.8",
|
"object-path": "^0.11.8",
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"sealious": "^0.19.9",
|
"sealious": "^0.19.36",
|
||||||
|
"slug": "^9.1.0",
|
||||||
"stimulus": "^3.2.2",
|
"stimulus": "^3.2.2",
|
||||||
"tempstream": "^0.4.1",
|
"tempstream": "^0.4.5",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"turndown": "^7.2.0"
|
"turndown": "^7.2.0"
|
||||||
},
|
},
|
||||||
@ -71,12 +112,15 @@
|
|||||||
"@types/koa__router": "^12.0.4",
|
"@types/koa__router": "^12.0.4",
|
||||||
"@types/node": "^20.8.4",
|
"@types/node": "^20.8.4",
|
||||||
"@types/object-path": "^0.11.4",
|
"@types/object-path": "^0.11.4",
|
||||||
|
"@types/slug": "^5.0.9",
|
||||||
"@types/tedious": "^4.0.7",
|
"@types/tedious": "^4.0.7",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
|
"@types/turndown": "^5.0.4",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "7.4",
|
"@typescript-eslint/eslint-plugin": "7.4",
|
||||||
"@typescript-eslint/parser": "7.4",
|
"@typescript-eslint/parser": "7.4",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
"eslint": "8.57",
|
"eslint": "8.57",
|
||||||
"eslint-config-prettier": "^7.2.0",
|
"eslint-config-prettier": "^7.2.0",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
@ -87,7 +131,7 @@
|
|||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"ts-loader": "^8.0.14",
|
"ts-loader": "^8.0.14",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^4.7"
|
"typescript": "^5.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=21.0.0"
|
"node": ">=21.0.0"
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import _locreq from "locreq";
|
import _locreq from "locreq";
|
||||||
|
import type { Context } from "koa";
|
||||||
|
import type { FlatTemplatable } from "tempstream";
|
||||||
import type { Context as SealiousContext } from "sealious";
|
import type { Context as SealiousContext } from "sealious";
|
||||||
import { App, LoggerMailer, SMTPMailer } from "sealious";
|
import { App, LoggerMailer, SMTPMailer } from "sealious";
|
||||||
import type { LoggerLevel } from "sealious/@types/src/app/logger.js";
|
import type { LoggerLevel } from "sealious/@types/src/app/logger.js";
|
||||||
@ -27,6 +29,25 @@ declare module "koa" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "@sealcode/sealgen" {
|
||||||
|
interface HTMLOptions {
|
||||||
|
preserveScroll?: boolean;
|
||||||
|
morphing?: boolean;
|
||||||
|
navbar?: (ctx: Context) => FlatTemplatable;
|
||||||
|
autoRefreshCSS?: boolean;
|
||||||
|
disableCopyEvent?: boolean;
|
||||||
|
language?: string;
|
||||||
|
showBanner?: boolean;
|
||||||
|
canHaveStreams?: boolean;
|
||||||
|
animateTitle?: boolean;
|
||||||
|
bodyClasses?: string[];
|
||||||
|
showFooter: boolean;
|
||||||
|
showBottomNavbar: boolean;
|
||||||
|
loadHamburgerMenu: boolean;
|
||||||
|
loadSearchModal: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class TheApp extends App {
|
export default class TheApp extends App {
|
||||||
FileManager = TheFileManager;
|
FileManager = TheFileManager;
|
||||||
config = {
|
config = {
|
||||||
|
@ -1,29 +1,32 @@
|
|||||||
// DO NOT EDIT! This file is generated automaticaly with 'npm run generate-collections'
|
// DO NOT EDIT! This file is generated automaticaly with 'npm run generate-collections'
|
||||||
import { App } from "sealious";
|
import { App } from "sealious";
|
||||||
|
|
||||||
|
import _Globals from "./globals.js";
|
||||||
import _GroupsToUsers from "./groups-to-users.js";
|
import _GroupsToUsers from "./groups-to-users.js";
|
||||||
import _Groups from "./groups.js";
|
import _Groups from "./groups.js";
|
||||||
|
import _Page from "./page.js";
|
||||||
import _PasswordResetIntents from "./password-reset-intents.js";
|
import _PasswordResetIntents from "./password-reset-intents.js";
|
||||||
import _Secrets from "./secrets.js";
|
import _Redirects from "./redirects.js";
|
||||||
import _Tasks from "./tasks.js";
|
|
||||||
import _UserRoles from "./user-roles.js";
|
import _UserRoles from "./user-roles.js";
|
||||||
import _Users from "./users.js";
|
import _Users from "./users.js";
|
||||||
|
|
||||||
|
export const Globals = new _Globals();
|
||||||
export const GroupsToUsers = new _GroupsToUsers();
|
export const GroupsToUsers = new _GroupsToUsers();
|
||||||
export const Groups = new _Groups();
|
export const Groups = new _Groups();
|
||||||
|
export const Page = new _Page();
|
||||||
export const PasswordResetIntents = new _PasswordResetIntents();
|
export const PasswordResetIntents = new _PasswordResetIntents();
|
||||||
export const Secrets = new _Secrets();
|
export const Redirects = new _Redirects();
|
||||||
export const Tasks = new _Tasks();
|
|
||||||
export const UserRoles = new _UserRoles();
|
export const UserRoles = new _UserRoles();
|
||||||
export const Users = new _Users();
|
export const Users = new _Users();
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
...App.BaseCollections,
|
...App.BaseCollections,
|
||||||
|
globals: Globals,
|
||||||
"groups-to-users": GroupsToUsers,
|
"groups-to-users": GroupsToUsers,
|
||||||
groups: Groups,
|
groups: Groups,
|
||||||
|
page: Page,
|
||||||
"password-reset-intents": PasswordResetIntents,
|
"password-reset-intents": PasswordResetIntents,
|
||||||
secrets: Secrets,
|
redirects: Redirects,
|
||||||
tasks: Tasks,
|
|
||||||
"user-roles": UserRoles,
|
"user-roles": UserRoles,
|
||||||
users: Users,
|
users: Users,
|
||||||
};
|
};
|
||||||
|
15
src/back/collections/globals.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
import { Roles } from "../policy-types/roles.js";
|
||||||
|
|
||||||
|
export default class Globals extends Collection {
|
||||||
|
fields = {
|
||||||
|
key: new FieldTypes.Text(),
|
||||||
|
value: new FieldTypes.Text(),
|
||||||
|
};
|
||||||
|
defaultPolicy = new Roles(["admin"]);
|
||||||
|
policies = {
|
||||||
|
show: new Policies.Public(),
|
||||||
|
delete: new Policies.Noone(),
|
||||||
|
create: new Policies.Super(),
|
||||||
|
};
|
||||||
|
}
|
16
src/back/collections/page.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
import { CRUDRoles } from "../policy-types/roles.js";
|
||||||
|
|
||||||
|
export default class Page extends Collection {
|
||||||
|
fields = {
|
||||||
|
url: new FieldTypes.Text().setRequired(true),
|
||||||
|
content: new FieldTypes.JDD().setRequired(true),
|
||||||
|
domain: new FieldTypes.Text(),
|
||||||
|
title: new FieldTypes.Text(),
|
||||||
|
heading: new FieldTypes.Text(),
|
||||||
|
description: new FieldTypes.Text(),
|
||||||
|
imageForMetadata: new FieldTypes.Image().setRequired(true),
|
||||||
|
hideNavigation: new FieldTypes.Boolean(),
|
||||||
|
};
|
||||||
|
policies = CRUDRoles(["page"], { show: new Policies.Public() });
|
||||||
|
}
|
12
src/back/collections/redirects.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
import { CRUDRoles, Roles } from "../policy-types/roles.js";
|
||||||
|
|
||||||
|
export default class Redirects extends Collection {
|
||||||
|
fields = {
|
||||||
|
from_url: new FieldTypes.Text().setRequired(true),
|
||||||
|
to_url: new FieldTypes.Text().setRequired(true),
|
||||||
|
type: new FieldTypes.Enum(["301", "302"]).setRequired(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
policies = CRUDRoles(["quick-filters"], { show: new Policies.Public() });
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
import { Collection, FieldTypes } from "sealious";
|
|
||||||
import { Roles } from "../policy-types/roles.js";
|
|
||||||
|
|
||||||
/* For testing the Roles policy */
|
|
||||||
export default class Secrets extends Collection {
|
|
||||||
fields = {
|
|
||||||
content: new FieldTypes.Text(),
|
|
||||||
};
|
|
||||||
defaultPolicy = new Roles(["admin"]);
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
@ -21,6 +21,7 @@ describe("user-roles", () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
(e) => {
|
(e) => {
|
||||||
|
console.error(e);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
e?.response.data.data.field_messages.role?.message,
|
e?.response.data.data.field_messages.role?.message,
|
||||||
"Missing value for field 'role'."
|
"Missing value for field 'role'."
|
||||||
|
@ -5,17 +5,18 @@ import { Roles } from "../policy-types/roles.js";
|
|||||||
export default class UserRoles extends Collection {
|
export default class UserRoles extends Collection {
|
||||||
name = "user-roles";
|
name = "user-roles";
|
||||||
fields = {
|
fields = {
|
||||||
role: new FieldTypes.Enum((app: App) =>
|
role: new FieldTypes.Text(),
|
||||||
app.ConfigManager.get("roles")
|
|
||||||
).setRequired(true),
|
|
||||||
user: new FieldTypes.SingleReference("users"),
|
user: new FieldTypes.SingleReference("users"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
policies = {
|
policies = {
|
||||||
create: new Roles(["admin"]),
|
create: new Roles(["admin"]),
|
||||||
delete: new Policies.Public(),
|
delete: new Roles(["admin"]),
|
||||||
show: new Policies.UserReferencedInField("user"),
|
show: new Policies.Or([
|
||||||
|
new Policies.UserReferencedInField("user"),
|
||||||
|
new Roles(["admin"]),
|
||||||
|
]),
|
||||||
edit: new Policies.Noone(),
|
edit: new Policies.Noone(),
|
||||||
} as { [policy: string]: Policy }; // this `as` statement allows the policies to be overwritten;
|
} as { [policy: string]: Policy }; // this `as` statement allows the policies to be overwritten;
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ export default class UserRoles extends Collection {
|
|||||||
throw new Error("roles undefined");
|
throw new Error("roles undefined");
|
||||||
}
|
}
|
||||||
for (const action of <const>["create", "delete"]) {
|
for (const action of <const>["create", "delete"]) {
|
||||||
const policy = roles.getPolicy(action);
|
const policy = roles?.getPolicy(action);
|
||||||
if (policy instanceof Policies.Public) {
|
if (policy instanceof Policies.Public) {
|
||||||
app.Logger.warn(
|
app.Logger.warn(
|
||||||
"USER POLICY",
|
"USER POLICY",
|
||||||
|
@ -3,6 +3,7 @@ import { App, Collections, FieldTypes, Policies } from "sealious";
|
|||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import TheApp from "../app.js";
|
import TheApp from "../app.js";
|
||||||
import ADMIN_CREDENTIALS from "../default-admin-credentials.js";
|
import ADMIN_CREDENTIALS from "../default-admin-credentials.js";
|
||||||
|
import { Roles } from "../policy-types/roles.js";
|
||||||
|
|
||||||
export default class Users extends Collections.users {
|
export default class Users extends Collections.users {
|
||||||
fields = {
|
fields = {
|
||||||
@ -14,18 +15,20 @@ export default class Users extends Collections.users {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultPolicy = new Policies.Themselves();
|
defaultPolicy = new Policies.Or([new Policies.Themselves(), new Roles(["admin"])]);
|
||||||
|
|
||||||
|
policies = {
|
||||||
|
create: new Roles(["admin"]),
|
||||||
|
show: new Policies.Or([new Policies.Themselves(), new Roles(["admin"])]),
|
||||||
|
};
|
||||||
|
|
||||||
async init(app: App, name: string) {
|
async init(app: App, name: string) {
|
||||||
assert(app instanceof TheApp);
|
assert(app instanceof TheApp);
|
||||||
await super.init(app, name);
|
await super.init(app, name);
|
||||||
app.on("started", async () => {
|
app.on("started", async () => {
|
||||||
const username = ADMIN_CREDENTIALS.username;
|
const username = ADMIN_CREDENTIALS.username;
|
||||||
const users = await app.collections.users
|
const all_users = await app.collections.users.suList().fetch();
|
||||||
.suList()
|
if (all_users.empty) {
|
||||||
.filter({ username })
|
|
||||||
.fetch();
|
|
||||||
if (users.empty) {
|
|
||||||
app.Logger.warn(
|
app.Logger.warn(
|
||||||
"ADMIN",
|
"ADMIN",
|
||||||
`Creating an admin account for ${app.manifest.admin_email}`
|
`Creating an admin account for ${app.manifest.admin_email}`
|
||||||
@ -42,15 +45,13 @@ export default class Users extends Collections.users {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getRoles(ctx: Context) {
|
public static async getRoles(ctx: Context) {
|
||||||
if (!ctx.app.collections["user-roles"]) {
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
throw new Error("user-roles undefined");
|
const rolesEntries = await (ctx.app as TheApp).collections["user-roles"]
|
||||||
}
|
|
||||||
const rolesEntries = await ctx.app.collections["user-roles"]
|
|
||||||
.list(ctx)
|
.list(ctx)
|
||||||
.filter({ user: ctx.user_id || "" })
|
.filter({ user: ctx.user_id || "" })
|
||||||
.fetch();
|
.fetch();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
return rolesEntries.items.map((item) => item.get("role") as string);
|
return rolesEntries.items.map((item) => item.get("role"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@ import _locreq from "locreq";
|
|||||||
import { module_dirname } from "./utils/module_dirname.js";
|
import { module_dirname } from "./utils/module_dirname.js";
|
||||||
|
|
||||||
const locreq = _locreq(module_dirname(import.meta.url));
|
const locreq = _locreq(module_dirname(import.meta.url));
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
export const SEALIOUS_SANITY = Boolean(process.env.SEALIOUS_SANITY);
|
export const SEALIOUS_SANITY = Boolean(process.env.SEALIOUS_SANITY);
|
||||||
export const PORT = process.env.SEALIOUS_PORT
|
export const PORT = process.env.SEALIOUS_PORT
|
||||||
? parseInt(process.env.SEALIOUS_PORT)
|
? parseInt(process.env.SEALIOUS_PORT)
|
||||||
@ -18,13 +22,14 @@ export const MAILCATCHER_SMTP_PORT = parseInt(
|
|||||||
export const MAILCATCHER_API_PORT = parseInt(
|
export const MAILCATCHER_API_PORT = parseInt(
|
||||||
process.env.SEALIOUS_MAILCATCHER_API_PORT || "1082"
|
process.env.SEALIOUS_MAILCATCHER_API_PORT || "1082"
|
||||||
);
|
);
|
||||||
export const MAILER = process.env.SEALIOUS_MAILER;
|
export const MAILER = process.env.SEALIOUS_MAILER || "mailcatcher";
|
||||||
export const DEFAULT_HTML_LANG = "pl";
|
export const DEFAULT_HTML_LANG = "pl";
|
||||||
|
|
||||||
export const IMAGE_CACHE_FS_DIR =
|
export const IMAGE_CACHE_FS_DIR =
|
||||||
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/images");
|
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/images");
|
||||||
export const SMARTCROP_CACHE_FS_DIR =
|
export const SMARTCROP_CACHE_FS_DIR =
|
||||||
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/smartcrop");
|
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/smartcrop");
|
||||||
|
|
||||||
export const UPLOADS_FS_DIR =
|
export const UPLOADS_FS_DIR =
|
||||||
process.env.UPLOADS_FS_DIR || locreq.resolve("uploaded_files");
|
process.env.UPLOADS_FS_DIR || locreq.resolve("uploaded_files");
|
||||||
|
export const MEILISEARCH_MASTER_KEY = process.env.MEILISEARCH_MASTER_KEY || "qwerty";
|
||||||
|
export const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST || "http://localhost:1098";
|
||||||
|
86
src/back/defaultHead.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import type { Readable } from "stream";
|
||||||
|
import type { Context } from "koa";
|
||||||
|
import type { HTMLOptions } from "@sealcode/sealgen";
|
||||||
|
import { htmlEscape } from "escape-goat";
|
||||||
|
|
||||||
|
export const start_timestamp = Date.now();
|
||||||
|
|
||||||
|
export const animation_script_head = /* HTML */ `<script type="text/javascript">
|
||||||
|
message = document.title + " • ";
|
||||||
|
window.title_animation_active = false;
|
||||||
|
window.animatePageTitle = function () {
|
||||||
|
if (window.title_animation_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.title_animation_active = true;
|
||||||
|
function step() {
|
||||||
|
message = message.substr(1) + message.substr(0, 1);
|
||||||
|
document.title = message;
|
||||||
|
}
|
||||||
|
setInterval(step, 100);
|
||||||
|
};
|
||||||
|
</script>`;
|
||||||
|
|
||||||
|
export function defaultHead({
|
||||||
|
ctx,
|
||||||
|
title,
|
||||||
|
htmlOptions,
|
||||||
|
metaImage,
|
||||||
|
canonicalPath,
|
||||||
|
css_clumps = [],
|
||||||
|
description = "",
|
||||||
|
}: {
|
||||||
|
ctx: Context;
|
||||||
|
title: string | Promise<string | Readable>;
|
||||||
|
htmlOptions: Partial<HTMLOptions>;
|
||||||
|
metaImage?: string;
|
||||||
|
canonicalPath?: string;
|
||||||
|
css_clumps: string[];
|
||||||
|
description: string;
|
||||||
|
}): JSX.Element | Readable {
|
||||||
|
const origin = ctx.URL.origin;
|
||||||
|
return tempstream/* HTML */ `<title>${title}</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="${htmlEscape(description)}" />
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/dist/fonts/Poppins-400-1.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/dist/fonts/Poppins-400-2.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
/>
|
||||||
|
<link rel="preload" href="/Exotc350DmBdEU-subset.ttf" as="font" type="font/ttf" />
|
||||||
|
<script defer src="/dist/bundle.js?v=${start_timestamp}"></script>
|
||||||
|
${metaImage ? `<meta property="og:image" content="${metaImage}" />` : ""}
|
||||||
|
${[
|
||||||
|
"default",
|
||||||
|
"page",
|
||||||
|
...(ctx.url.includes("/dowodzenie/") ? ["admin"] : []),
|
||||||
|
...css_clumps,
|
||||||
|
].map(
|
||||||
|
(clump_name) => /* HTML*/ `<link
|
||||||
|
href="/dist/${clump_name}.entrypoint.css?v=${start_timestamp}${
|
||||||
|
htmlOptions.autoRefreshCSS ? `?${Math.random()}${Math.random()}` : ""
|
||||||
|
}"
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
/>`
|
||||||
|
)}
|
||||||
|
<link href="/dist/fonts/fonts.css" rel="stylesheet" type="text/css" />
|
||||||
|
${canonicalPath
|
||||||
|
? `<link rel="canonical" href="${origin}${canonicalPath}" />`
|
||||||
|
: ""}
|
||||||
|
${htmlOptions.morphing
|
||||||
|
? `<meta name="turbo-refresh-method" content="morph" />`
|
||||||
|
: ""}
|
||||||
|
${htmlOptions.preserveScroll
|
||||||
|
? `<meta name="turbo-refresh-scroll" content="preserve">`
|
||||||
|
: ""} `;
|
||||||
|
}
|
@ -13,10 +13,8 @@ export default async function PasswordResetTemplate(
|
|||||||
if (!matching_users.items.length) {
|
if (!matching_users.items.length) {
|
||||||
throw new Errors.NotFound("No user with that email");
|
throw new Errors.NotFound("No user with that email");
|
||||||
}
|
}
|
||||||
if (!matching_users.items[0]) {
|
|
||||||
throw new Error("matching_users.items[0] undefined");
|
const username = matching_users.items[0]?.get("username");
|
||||||
}
|
|
||||||
const username = matching_users.items[0].get("username");
|
|
||||||
|
|
||||||
return EmailTemplates.Simple(app, {
|
return EmailTemplates.Simple(app, {
|
||||||
subject: app.i18n("password_reset_email_subject", [app.manifest.name]),
|
subject: app.i18n("password_reset_email_subject", [app.manifest.name]),
|
||||||
|
@ -1,112 +0,0 @@
|
|||||||
import { Controller } from "stimulus";
|
|
||||||
|
|
||||||
const APP_DOWN_ERROR_MESSAGE = "App is currently down";
|
|
||||||
|
|
||||||
const sleep = (time: number) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, time);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function get_status(): Promise<{ started_at: number; status: string }> {
|
|
||||||
const r = await fetch("/status.json");
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
return (await r.json()) as { started_at: number; status: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function wait_for_run_id_to_change() {
|
|
||||||
let first_timestamp: number;
|
|
||||||
try {
|
|
||||||
const { started_at } = await get_status();
|
|
||||||
first_timestamp = started_at;
|
|
||||||
} catch (e) {
|
|
||||||
await wait_for_app_to_be_stable();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!first_timestamp) {
|
|
||||||
throw new Error(APP_DOWN_ERROR_MESSAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const { started_at } = await get_status().catch(() => ({
|
|
||||||
started_at: first_timestamp,
|
|
||||||
}));
|
|
||||||
if (started_at !== first_timestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await sleep(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function wait_for_app_to_be_stable(n = 3) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("Waiting for app to be stable....");
|
|
||||||
let counter = 0;
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const { status } = await get_status().catch(() => ({
|
|
||||||
status: "down",
|
|
||||||
}));
|
|
||||||
if (status == "running") {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(counter);
|
|
||||||
counter++;
|
|
||||||
} else {
|
|
||||||
counter = 0;
|
|
||||||
}
|
|
||||||
if (counter == n) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await sleep(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function wait_for_app_restart() {
|
|
||||||
try {
|
|
||||||
await wait_for_run_id_to_change();
|
|
||||||
} catch (e) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
if (e.message !== APP_DOWN_ERROR_MESSAGE) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await wait_for_app_to_be_stable();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RefreshOnTSChanges extends Controller {
|
|
||||||
socket: WebSocket;
|
|
||||||
|
|
||||||
async connect() {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const { port, watch } = (await fetch("/dist/notifier.json").then((r) =>
|
|
||||||
r.json()
|
|
||||||
)) as { port: number; watch: boolean };
|
|
||||||
if (!watch) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(
|
|
||||||
"Not running auto refresh on watch because the build process is not running in watch mode"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const socket = new WebSocket(`ws://localhost:${port}`);
|
|
||||||
socket.onmessage = async (message) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const data = message.data as unknown;
|
|
||||||
if (typeof data === "string" && data.endsWith("-ts")) {
|
|
||||||
document.documentElement.classList.add("restarting");
|
|
||||||
await wait_for_app_restart();
|
|
||||||
document.documentElement.dispatchEvent(new Event("ts-rebuilt"));
|
|
||||||
document.documentElement.classList.remove("restarting");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async disconnect() {
|
|
||||||
this.socket.close();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
import { Controller } from "stimulus";
|
|
||||||
|
|
||||||
function make_new_link() {
|
|
||||||
const new_link = document.createElement("link");
|
|
||||||
new_link.rel = "stylesheet";
|
|
||||||
new_link.href = `/dist/main.css?${Math.random()}+${Math.random()}`;
|
|
||||||
new_link.type = "text/css";
|
|
||||||
return new_link;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStyles() {
|
|
||||||
return Array.from(document.querySelectorAll("head link")).filter(
|
|
||||||
(e: HTMLLinkElement) => new URL(e.href).pathname == "/dist/main.css"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup_css() {
|
|
||||||
getStyles()
|
|
||||||
.slice(0, -1)
|
|
||||||
.forEach((style) => {
|
|
||||||
style.parentElement.removeChild(style);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RefreshStyles extends Controller {
|
|
||||||
socket: WebSocket;
|
|
||||||
|
|
||||||
async connect() {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const { port } = await fetch("/dist/notifier.json").then((r) => r.json());
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
this.socket = new WebSocket(`ws://localhost:${port as number}`);
|
|
||||||
this.socket.onmessage = async (message) => {
|
|
||||||
if (message.data === "css") {
|
|
||||||
const new_link = make_new_link();
|
|
||||||
new_link.onload = cleanup_css;
|
|
||||||
document.querySelector("head").appendChild(new_link);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.documentElement.addEventListener("turbo:morph", cleanup_css);
|
|
||||||
}
|
|
||||||
|
|
||||||
async disconnect() {
|
|
||||||
this.socket.close();
|
|
||||||
document.documentElement.removeEventListener("turbo:morph", cleanup_css);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
import type { FlatTemplatable, Templatable } from "tempstream";
|
|
||||||
import { tempstreamAsync } from "tempstream";
|
|
||||||
import type { Readable } from "stream";
|
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import { toKebabCase } from "js-convert-case";
|
|
||||||
import { DEFAULT_HTML_LANG } from "./config.js";
|
|
||||||
import { default_navbar } from "./routes/common/navbar.js";
|
|
||||||
|
|
||||||
export const defaultHead = (
|
|
||||||
ctx: BaseContext,
|
|
||||||
title: string,
|
|
||||||
options: HTMLOptions
|
|
||||||
) => /* HTML */ `<title>${title} · ${ctx.$app.manifest.name}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<script async src="/dist/bundle.js"></script>
|
|
||||||
<link
|
|
||||||
href="/dist/main.css${options.autoRefreshCSS
|
|
||||||
? `?${Math.random()}${Math.random()}`
|
|
||||||
: ""}"
|
|
||||||
rel="stylesheet"
|
|
||||||
type="text/css"
|
|
||||||
/>
|
|
||||||
<link href="/dist/fonts/fonts.css" rel="stylesheet" type="text/css" />
|
|
||||||
${options.morphing ? `<meta name="turbo-refresh-method" content="morph" />` : ""}
|
|
||||||
${options.preserveScroll
|
|
||||||
? `<meta name="turbo-refresh-scroll" content="preserve">`
|
|
||||||
: ""}`;
|
|
||||||
|
|
||||||
export type HTMLOptions = {
|
|
||||||
preserveScroll?: boolean;
|
|
||||||
morphing?: boolean;
|
|
||||||
navbar?: (ctx: BaseContext) => FlatTemplatable;
|
|
||||||
autoRefreshCSS?: boolean;
|
|
||||||
disableCopyEvent?: boolean;
|
|
||||||
language?: string;
|
|
||||||
bodyClasses?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function html(
|
|
||||||
ctx: BaseContext,
|
|
||||||
title: string,
|
|
||||||
body: Templatable,
|
|
||||||
htmlOptions: HTMLOptions = {},
|
|
||||||
makeHead: (
|
|
||||||
ctx: BaseContext,
|
|
||||||
title: string,
|
|
||||||
options: HTMLOptions
|
|
||||||
) => Templatable = defaultHead
|
|
||||||
): Readable {
|
|
||||||
ctx.set("content-type", "text/html;charset=utf-8");
|
|
||||||
const controllers: string[] = [];
|
|
||||||
if (htmlOptions.autoRefreshCSS) {
|
|
||||||
controllers.push("refresh-styles");
|
|
||||||
controllers.push("refresh-on-ts-changes");
|
|
||||||
}
|
|
||||||
return tempstreamAsync/* HTML */ ` <!DOCTYPE html>
|
|
||||||
<html
|
|
||||||
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
|
|
||||||
class="title--${toKebabCase(title)}"
|
|
||||||
>
|
|
||||||
<head>
|
|
||||||
${makeHead(ctx, title, htmlOptions)}
|
|
||||||
</head>
|
|
||||||
<body data-controller="${controllers.join(" ")}">
|
|
||||||
${(htmlOptions?.navbar || default_navbar)(ctx)} ${body}
|
|
||||||
${htmlOptions.disableCopyEvent
|
|
||||||
? /* HTML */ "<script>document.addEventListener('copy', (e) => e.preventDefault());</script>"
|
|
||||||
: ""}
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
71
src/back/html.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import type { HTMLArgs, HTMLOptions } from "@sealcode/sealgen";
|
||||||
|
import { tempstreamAsync } from "tempstream";
|
||||||
|
import type { Readable } from "stream";
|
||||||
|
import { toKebabCase } from "js-convert-case";
|
||||||
|
import { DEFAULT_HTML_LANG } from "./config.js";
|
||||||
|
import { default_navbar } from "./routes/common/navbar.js";
|
||||||
|
import { defaultHead } from "./defaultHead.js";
|
||||||
|
|
||||||
|
const default_html_options: Partial<HTMLOptions> = {
|
||||||
|
showFooter: true,
|
||||||
|
showBottomNavbar: true,
|
||||||
|
loadHamburgerMenu: true,
|
||||||
|
loadSearchModal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function html({
|
||||||
|
ctx,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
htmlOptions = {},
|
||||||
|
makeHead = defaultHead,
|
||||||
|
metaImage,
|
||||||
|
canonicalPath,
|
||||||
|
css_clumps = [],
|
||||||
|
description,
|
||||||
|
hideNavigation = false,
|
||||||
|
}: HTMLArgs): Readable {
|
||||||
|
htmlOptions = { ...default_html_options, ...htmlOptions };
|
||||||
|
ctx.set(
|
||||||
|
"content-type",
|
||||||
|
htmlOptions.canHaveStreams &&
|
||||||
|
ctx.get("accept").includes("text/vnd.turbo-stream.html")
|
||||||
|
? "text/vnd.turbo-stream.html"
|
||||||
|
: "text/html;charset=utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
const controllers: string[] = [];
|
||||||
|
if (htmlOptions.autoRefreshCSS) {
|
||||||
|
controllers.push("refresh-styles");
|
||||||
|
controllers.push("refresh-on-ts-changes");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempstreamAsync/* HTML */ ` <!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
|
||||||
|
class="title--${typeof title == "string" ? toKebabCase(title) : ""}"
|
||||||
|
>
|
||||||
|
<head>
|
||||||
|
<meta
|
||||||
|
name="google-site-verification"
|
||||||
|
content="Tf9a888qUgbgQJS4u1F6A8H7ZI_DBJvAeduluSI0T_g"
|
||||||
|
/>
|
||||||
|
${makeHead({
|
||||||
|
ctx,
|
||||||
|
title: title,
|
||||||
|
htmlOptions: { ...htmlOptions },
|
||||||
|
metaImage,
|
||||||
|
canonicalPath,
|
||||||
|
css_clumps,
|
||||||
|
description,
|
||||||
|
})}
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
data-controller="${controllers.join(" ")}"
|
||||||
|
class="${(htmlOptions?.bodyClasses || []).join(" ")}"
|
||||||
|
>
|
||||||
|
${!hideNavigation ? (htmlOptions?.navbar || default_navbar)(ctx) : ""}
|
||||||
|
${body}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
@ -9,4 +9,6 @@ export const imageRouter = new KoaResponsiveImageRouter({
|
|||||||
cacheManagerResolutionThreshold: 50,
|
cacheManagerResolutionThreshold: 50,
|
||||||
imageStoragePath: IMAGE_CACHE_FS_DIR,
|
imageStoragePath: IMAGE_CACHE_FS_DIR,
|
||||||
smartCropStoragePath: SMARTCROP_CACHE_FS_DIR,
|
smartCropStoragePath: SMARTCROP_CACHE_FS_DIR,
|
||||||
|
formatsForLossy: ["webp"],
|
||||||
|
formatsForLossless: ["webp"],
|
||||||
});
|
});
|
||||||
|
@ -3,9 +3,9 @@ import _locreq from "locreq";
|
|||||||
import TheApp from "./app.js";
|
import TheApp from "./app.js";
|
||||||
import { PORT, SEALIOUS_SANITY } from "./config.js";
|
import { PORT, SEALIOUS_SANITY } from "./config.js";
|
||||||
import { mainRouter } from "./routes/index.js";
|
import { mainRouter } from "./routes/index.js";
|
||||||
import { module_dirname } from "./utils/module_dirname.js";
|
import { waitForMeilisearch } from "./services/meilisearch.js";
|
||||||
|
|
||||||
const locreq = _locreq(module_dirname(import.meta.url));
|
const locreq = _locreq(new URL("./", import.meta.url).pathname);
|
||||||
|
|
||||||
export const the_app = new TheApp();
|
export const the_app = new TheApp();
|
||||||
|
|
||||||
@ -13,14 +13,31 @@ void (async function () {
|
|||||||
await kill(PORT);
|
await kill(PORT);
|
||||||
await kill(PORT);
|
await kill(PORT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForMeilisearch(10);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
if ((e as Error).message === "timeout") {
|
||||||
|
console.log("Meilisearch must be running before the app starts!"); // eslint-disable-line no-console
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
the_app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
the_app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
||||||
|
|
||||||
await the_app.start();
|
await the_app.start();
|
||||||
|
mainRouter(the_app, the_app.HTTPServer.router);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("Population finished");
|
||||||
|
|
||||||
if (SEALIOUS_SANITY) {
|
if (SEALIOUS_SANITY) {
|
||||||
console.error("Exiting with error code 0");
|
console.error("Exiting with error code 0");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
mainRouter(the_app.HTTPServer.router);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (SEALIOUS_SANITY) {
|
if (SEALIOUS_SANITY) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.autoscrolling-images {
|
.autoscrolling-images {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin: 2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoscrolling-images-wrapper {
|
.autoscrolling-images-wrapper {
|
||||||
@ -17,6 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.autoscrolling-images__title {
|
.autoscrolling-images__title {
|
||||||
|
font-family: Exotc350DmBdEU;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: #0d4d69;
|
color: #0d4d69;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -60,7 +62,13 @@
|
|||||||
|
|
||||||
.autoscrolling-images__carousel-container {
|
.autoscrolling-images__carousel-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 940px;
|
max-width: min(940px, calc(100dvw - 48px));
|
||||||
|
|
||||||
|
picture,
|
||||||
|
img {
|
||||||
|
max-height: 150px;
|
||||||
|
max-width: 288px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoscrolling-images__imgs-carousel {
|
.autoscrolling-images__imgs-carousel {
|
||||||
@ -71,6 +79,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
|
transition: transform 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoscrolling-images__carousel-page {
|
.autoscrolling-images__carousel-page {
|
||||||
@ -83,10 +92,28 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
|
column-gap: 1rem;
|
||||||
|
|
||||||
max-width: 940px;
|
max-width: 940px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.autoscrolling-images__carousel-page--looping-head,
|
||||||
|
.autoscrolling-images__carousel-page--looping-tail {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoscrolling-images__carousel-page--looping-tail {
|
||||||
|
/* background-color: red; */
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoscrolling-images__carousel-page--looping-head {
|
||||||
|
/* background-color: green; */
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.autoscrolling-images__img-wrapper {
|
.autoscrolling-images__img-wrapper {
|
||||||
width: 288px;
|
width: 288px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
@ -123,11 +150,8 @@
|
|||||||
0% {
|
0% {
|
||||||
transform: translateX(0%);
|
transform: translateX(0%);
|
||||||
}
|
}
|
||||||
50% {
|
|
||||||
transform: translateX(calc(-100% + 100cqw));
|
|
||||||
}
|
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(0%);
|
transform: translateX(calc(-100%));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +196,11 @@
|
|||||||
animation-name: autoscrolling-images-infiniteScroll;
|
animation-name: autoscrolling-images-infiniteScroll;
|
||||||
animation-duration: var(--animation-length);
|
animation-duration: var(--animation-length);
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoscrolling-images__carousel-page--looping-head,
|
||||||
|
.autoscrolling-images__carousel-page--looping-tail {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,156 @@
|
|||||||
import { TempstreamJSX } from "tempstream";
|
import { TempstreamJSX } from "tempstream";
|
||||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
import type {
|
||||||
|
ComponentToHTMLArgs,
|
||||||
|
ExtractStructuredComponentArgumentsParsed,
|
||||||
|
JDDContext,
|
||||||
|
} from "@sealcode/jdd";
|
||||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
|
|
||||||
import arrow from "./autoscrolling-images-arrow.svg";
|
import arrow from "./autoscrolling-images-arrow.svg";
|
||||||
|
|
||||||
const component_arguments = {
|
import type { FilePointer } from "@sealcode/file-manager";
|
||||||
title: new ComponentArguments.ShortText(),
|
import type { makeJDDContext } from "../../jdd-context.js";
|
||||||
interval: new ComponentArguments.ShortText().setExampleValues(["5"]),
|
import _locreq from "locreq";
|
||||||
imagesPerPage: new ComponentArguments.ShortText().setExampleValues(["6"]),
|
const locreq = _locreq(new URL("./", import.meta.url).pathname);
|
||||||
images: new ComponentArguments.List(
|
|
||||||
|
const images = new ComponentArguments.List(
|
||||||
new ComponentArguments.Structured({
|
new ComponentArguments.Structured({
|
||||||
image: new ComponentArguments.Image(),
|
image: new ComponentArguments.Image().setExampleImages(
|
||||||
|
[
|
||||||
|
"logo1.png",
|
||||||
|
"logo2.png",
|
||||||
|
"logo3.png",
|
||||||
|
"logo4.png",
|
||||||
|
"logo5.png",
|
||||||
|
"logo6.png",
|
||||||
|
"logo7.png",
|
||||||
|
"logo8.png",
|
||||||
|
"logo9.png",
|
||||||
|
].map((name) => locreq.resolve("assets/" + name))
|
||||||
|
),
|
||||||
alt: new ComponentArguments.ShortText(),
|
alt: new ComponentArguments.ShortText(),
|
||||||
})
|
})
|
||||||
),
|
);
|
||||||
|
|
||||||
|
images.getExampleCount = () => {
|
||||||
|
return Math.round(4 + Math.random() * 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
const component_arguments = {
|
||||||
|
title: new ComponentArguments.ShortText(),
|
||||||
|
imagesPerPage: new ComponentArguments.ShortText().setExampleValues(["6"]),
|
||||||
|
speed: new ComponentArguments.ShortText().setExampleValues(["50"]),
|
||||||
|
desktop_interval: new ComponentArguments.ShortText().setExampleValues(["5"]),
|
||||||
|
images,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
function renderimagePageArrows({
|
||||||
|
radioButtonIdPrefix,
|
||||||
|
pageIndex,
|
||||||
|
imagePages,
|
||||||
|
}: {
|
||||||
|
radioButtonIdPrefix: string;
|
||||||
|
pageIndex: number;
|
||||||
|
imagePages: Array<Array<Record<string, unknown>>>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="autoscrolling-images__arrow-container">
|
||||||
|
<label
|
||||||
|
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
|
||||||
|
pageIndex == 0 ? imagePages.length - 1 : pageIndex - 1
|
||||||
|
}`}
|
||||||
|
class="autoscrolling-images__arrow"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="autoscrolling-images__img-arrow-left"
|
||||||
|
src={arrow.url}
|
||||||
|
alt="←"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
|
||||||
|
pageIndex == imagePages.length - 1 ? 0 : pageIndex + 1
|
||||||
|
}`}
|
||||||
|
class="autoscrolling-images__arrow"
|
||||||
|
>
|
||||||
|
<img src={arrow.url} alt="→" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderImagePage({
|
||||||
|
page,
|
||||||
|
render_image,
|
||||||
|
mode = "regular",
|
||||||
|
}: {
|
||||||
|
page: Array<{ image: FilePointer | null; alt: string }>;
|
||||||
|
render_image: ReturnType<typeof makeJDDContext>["render_image"];
|
||||||
|
mode?: "regular" | "looping-tail" | "looping-head";
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
"autoscrolling-images__carousel-page",
|
||||||
|
`autoscrolling-images__carousel-page--${mode}`,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{page.map((image) => (
|
||||||
|
<div class="autoscrolling-images__img-wrapper">
|
||||||
|
{render_image(image.image, {
|
||||||
|
container: {
|
||||||
|
width: 212,
|
||||||
|
height: 150,
|
||||||
|
objectFit: "contain",
|
||||||
|
},
|
||||||
|
alt: image.alt,
|
||||||
|
lossless: true,
|
||||||
|
thumbnailSize: 0,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export class AutoscrollingImages extends Component<typeof component_arguments> {
|
export class AutoscrollingImages extends Component<typeof component_arguments> {
|
||||||
getArguments() {
|
getArguments() {
|
||||||
return component_arguments;
|
return component_arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTitle(
|
||||||
|
_: JDDContext,
|
||||||
|
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||||
|
) {
|
||||||
|
return args.title || null;
|
||||||
|
}
|
||||||
|
|
||||||
toHTML({
|
toHTML({
|
||||||
args: { title, interval, imagesPerPage, images },
|
args: { title, imagesPerPage, images, speed, desktop_interval },
|
||||||
jdd_context: { render_image },
|
jdd_context: { render_image },
|
||||||
|
classes,
|
||||||
|
index,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>) {
|
}: ComponentToHTMLArgs<typeof component_arguments>) {
|
||||||
const imageNumberPerPage = parseInt(imagesPerPage);
|
const imageNumberPerPage = parseInt(imagesPerPage);
|
||||||
const parsedImagesArray = [];
|
const imagePages: (typeof images)[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < images.length; i += imageNumberPerPage) {
|
for (let i = 0; i < images.length; i += imageNumberPerPage) {
|
||||||
parsedImagesArray.push(images.slice(i, i + imageNumberPerPage));
|
imagePages.push(images.slice(i, i + imageNumberPerPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
const radioButtonIdPrefix = "r" + Math.floor(100 + Math.random() * 900);
|
const radioButtonIdPrefix = "r" + Math.floor(100 + Math.random() * 900);
|
||||||
const radioButton_name = `autoscrolling-images__radio--${radioButtonIdPrefix}`;
|
const radioButton_name = `autoscrolling-images__radio--${radioButtonIdPrefix}`;
|
||||||
const numberOfImages = images.length * 5;
|
|
||||||
const titleUpperCase = title.toUpperCase();
|
const titleUpperCase = title.toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="autoscrolling-images"
|
class={["autoscrolling-images", ...classes]}
|
||||||
|
style={`--jdd-index: ${index}`}
|
||||||
data-controller="autoscrolling-images"
|
data-controller="autoscrolling-images"
|
||||||
data-autoscrolling-images-interval={interval}
|
data-autoscrolling-images-interval={desktop_interval}
|
||||||
>
|
>
|
||||||
<style>
|
<style>
|
||||||
{parsedImagesArray
|
{imagePages
|
||||||
.map(
|
.map(
|
||||||
(_, pageIndex) =>
|
(_, pageIndex) =>
|
||||||
`#${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}:checked ~ .autoscrolling-images__imgs-carousel > .autoscrolling-images__carousel {
|
`#${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}:checked ~ .autoscrolling-images__imgs-carousel > .autoscrolling-images__carousel {
|
||||||
@ -75,7 +181,7 @@ export class AutoscrollingImages extends Component<typeof component_arguments> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="autoscrolling-images__carousel-container">
|
<div class="autoscrolling-images__carousel-container">
|
||||||
{parsedImagesArray.map((_, pageIndex) => (
|
{imagePages.map((_, pageIndex) => (
|
||||||
<input
|
<input
|
||||||
class="autoscrolling-images__radio"
|
class="autoscrolling-images__radio"
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -84,72 +190,62 @@ export class AutoscrollingImages extends Component<typeof component_arguments> {
|
|||||||
value={pageIndex}
|
value={pageIndex}
|
||||||
id={`${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}`}
|
id={`${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}`}
|
||||||
checked={pageIndex === 0}
|
checked={pageIndex === 0}
|
||||||
data-action="autoscrolling-images#handleRadiochange"
|
data-action="autoscrolling-images#handleRadioChange"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{imagePages.length > 1 ? (
|
||||||
<div class="autoscrolling-images__arrow-carousel-container">
|
<div class="autoscrolling-images__arrow-carousel-container">
|
||||||
<div class="autoscrolling-images__arrow-carousel">
|
<div class="autoscrolling-images__arrow-carousel">
|
||||||
{parsedImagesArray.map((_, pageIndex) => (
|
{imagePages.map((_, pageIndex) =>
|
||||||
<div class="autoscrolling-images__arrow-container">
|
renderimagePageArrows({
|
||||||
<label
|
pageIndex,
|
||||||
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
|
imagePages,
|
||||||
pageIndex == 0
|
radioButtonIdPrefix,
|
||||||
? parsedImagesArray.length - 1
|
})
|
||||||
: pageIndex - 1
|
)}
|
||||||
}`}
|
|
||||||
class="autoscrolling-images__arrow"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="autoscrolling-images__img-arrow-left"
|
|
||||||
src={arrow.url}
|
|
||||||
alt="←"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
|
|
||||||
pageIndex == parsedImagesArray.length - 1
|
|
||||||
? 0
|
|
||||||
: pageIndex + 1
|
|
||||||
}`}
|
|
||||||
class="autoscrolling-images__arrow"
|
|
||||||
>
|
|
||||||
<img src={arrow.url} alt="→" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
<div class="autoscrolling-images__imgs-carousel">
|
<div class="autoscrolling-images__imgs-carousel">
|
||||||
<div
|
<div
|
||||||
class="autoscrolling-images__carousel"
|
class="autoscrolling-images__carousel"
|
||||||
style={`--animation-length: ${numberOfImages}s`}
|
style={`--animation-length: ${
|
||||||
|
100 / parseInt(speed || "15")
|
||||||
|
}s`}
|
||||||
>
|
>
|
||||||
{parsedImagesArray.map((page) => (
|
{renderImagePage({
|
||||||
<div class="autoscrolling-images__carousel-page">
|
page: imagePages.at(-1)!,
|
||||||
{page.map((image) => (
|
render_image,
|
||||||
<div class="autoscrolling-images__img-wrapper">
|
mode: "looping-head",
|
||||||
{render_image(image.image, {
|
|
||||||
container: {
|
|
||||||
width: 288,
|
|
||||||
height: 150,
|
|
||||||
objectFit: "contain",
|
|
||||||
},
|
|
||||||
alt: image.alt,
|
|
||||||
})}
|
})}
|
||||||
</div>
|
{imagePages.map((page) =>
|
||||||
))}
|
renderImagePage({ page, render_image })
|
||||||
</div>
|
)}
|
||||||
))}
|
{/* for looping endless scroll*/}
|
||||||
|
{imagePages[0]
|
||||||
|
? renderImagePage({
|
||||||
|
page: imagePages[0],
|
||||||
|
render_image,
|
||||||
|
mode: "looping-tail",
|
||||||
|
})
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{imagePages.length > 1 ? (
|
||||||
<div class="autoscrolling-images__dots-container">
|
<div class="autoscrolling-images__dots-container">
|
||||||
{parsedImagesArray.map((_, pageIndex) => (
|
{imagePages.map((_, pageIndex) => (
|
||||||
<label
|
<label
|
||||||
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}`}
|
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}`}
|
||||||
class="autoscrolling-images__dots"
|
class="autoscrolling-images__dots"
|
||||||
></label>
|
></label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||||
import { Controller } from "stimulus";
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
export default class AutoscrollingImages extends Controller {
|
export default class AutoscrollingImages extends Controller {
|
||||||
@ -5,9 +6,10 @@ export default class AutoscrollingImages extends Controller {
|
|||||||
interval_id: number;
|
interval_id: number;
|
||||||
|
|
||||||
getInterval(): number {
|
getInterval(): number {
|
||||||
return parseInt(
|
const result = parseInt(
|
||||||
this.element.getAttribute("data-autoscrolling-images-interval") || "5"
|
this.element.getAttribute("data-autoscrolling-images-interval") || "3"
|
||||||
);
|
);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRadioButtons(): Array<HTMLInputElement> {
|
getRadioButtons(): Array<HTMLInputElement> {
|
||||||
@ -26,7 +28,7 @@ export default class AutoscrollingImages extends Controller {
|
|||||||
|
|
||||||
let intervalTime: number;
|
let intervalTime: number;
|
||||||
const interval = this.getInterval();
|
const interval = this.getInterval();
|
||||||
if (!interval) {
|
if (interval) {
|
||||||
intervalTime = interval * 1000;
|
intervalTime = interval * 1000;
|
||||||
} else {
|
} else {
|
||||||
intervalTime = 5000;
|
intervalTime = 5000;
|
||||||
@ -45,23 +47,20 @@ export default class AutoscrollingImages extends Controller {
|
|||||||
|
|
||||||
next_slide() {
|
next_slide() {
|
||||||
const carouselPages = this.element.querySelectorAll(
|
const carouselPages = this.element.querySelectorAll(
|
||||||
".autoscrolling-images__carousel-page"
|
".autoscrolling-images__carousel-page--regular"
|
||||||
);
|
);
|
||||||
|
|
||||||
const radioButtonIdPrefix = this.element
|
const radioButtonIdPrefix = this.element
|
||||||
.querySelector(".autoscrolling-images-wrapper")
|
.querySelector(".autoscrolling-images-wrapper")
|
||||||
.getAttribute("data-carousel-id-prefix");
|
.getAttribute("data-carousel-id-prefix");
|
||||||
|
|
||||||
const nextIndex = (this.currentIndex + 1) % carouselPages.length;
|
const nextIndex = (this.currentIndex + 1) % carouselPages.length;
|
||||||
|
const nextButtonID =
|
||||||
const nextButton =
|
|
||||||
radioButtonIdPrefix + "-autoscrolling-images__radio-" + nextIndex;
|
radioButtonIdPrefix + "-autoscrolling-images__radio-" + nextIndex;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
(document.getElementById(nextButton) as HTMLInputElement).checked = true;
|
const nextButton = this.element.querySelector("#" + nextButtonID);
|
||||||
|
if (nextButton) {
|
||||||
|
(nextButton as HTMLInputElement).checked = true;
|
||||||
this.currentIndex = nextIndex;
|
this.currentIndex = nextIndex;
|
||||||
|
this.handleRadioChange();
|
||||||
// this.handleRadioChange();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,11 @@
|
|||||||
background: #eee;
|
background: #eee;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
|
||||||
transition: border 200ms, color 200ms;
|
transition: border 200ms, color 200ms;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding: 12px 15px;
|
padding: 12px 15px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabs-menu-button:hover {
|
.tabs-menu-button:hover {
|
||||||
@ -30,13 +30,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-menu-radio:checked ~ .tabs-menu-button label {
|
|
||||||
border-bottom-color: #fff;
|
|
||||||
border-top-color: #b721ff;
|
|
||||||
background: #fff;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs-menu-radio {
|
.tabs-menu-radio {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -55,6 +48,11 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #000000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tile-content {
|
.tile-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #c3c3c3;
|
background-color: #c3c3c3;
|
||||||
@ -223,9 +221,13 @@
|
|||||||
.tab-container {
|
.tab-container {
|
||||||
.tiles-container {
|
.tiles-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
max-width: 450px;
|
max-width: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
max-height: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
.tile.horizontal {
|
.tile.horizontal {
|
||||||
grid-column: span 1;
|
grid-column: span 1;
|
||||||
grid-row: span 1;
|
grid-row: span 1;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import type { FlatTemplatable } from "tempstream";
|
import type { FlatTemplatable } from "tempstream";
|
||||||
import { TempstreamJSX } from "tempstream";
|
import { TempstreamJSX } from "tempstream";
|
||||||
import type { ComponentToHTMLArgs, ExtractParsed, JDDContext } from "@sealcode/jdd";
|
import type {
|
||||||
|
ComponentToHTMLArgs,
|
||||||
|
ExtractParsed,
|
||||||
|
ExtractStructuredComponentArgumentsParsed,
|
||||||
|
JDDContext,
|
||||||
|
} from "@sealcode/jdd";
|
||||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
|
|
||||||
type ExtractArray<T> = T extends Array<infer X> ? X : never;
|
type ExtractArray<T> = T extends Array<infer X> ? X : never;
|
||||||
@ -58,8 +63,15 @@ export class DynamicGrid extends Component<typeof component_arguments> {
|
|||||||
return component_arguments;
|
return component_arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTitle(
|
||||||
|
_: JDDContext,
|
||||||
|
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||||
|
) {
|
||||||
|
return args.heading || null;
|
||||||
|
}
|
||||||
|
|
||||||
public image_sizes = {
|
public image_sizes = {
|
||||||
square: { width: 400, height: 400 },
|
square: { width: 700, height: 700 },
|
||||||
horizontal: { width: 824, height: 400 },
|
horizontal: { width: 824, height: 400 },
|
||||||
vertical: { width: 400, height: 824 },
|
vertical: { width: 400, height: 824 },
|
||||||
};
|
};
|
||||||
@ -72,6 +84,7 @@ export class DynamicGrid extends Component<typeof component_arguments> {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div class={["tile", tile.shape]}>
|
<div class={["tile", tile.shape]}>
|
||||||
|
<a href={[tile.url]}>
|
||||||
<div class="tile-content">
|
<div class="tile-content">
|
||||||
<div class="tile-content-wrapper">
|
<div class="tile-content-wrapper">
|
||||||
<h3 class="tile-title">{tile.title}</h3>
|
<h3 class="tile-title">{tile.title}</h3>
|
||||||
@ -82,8 +95,11 @@ export class DynamicGrid extends Component<typeof component_arguments> {
|
|||||||
|
|
||||||
{(["square", "horizontal", "vertical"] as const).map((shape) => (
|
{(["square", "horizontal", "vertical"] as const).map((shape) => (
|
||||||
<div class={["tile-image", `tile-image--${shape}`]}>
|
<div class={["tile-image", `tile-image--${shape}`]}>
|
||||||
{jdd_context.render_image(tile.photo.image, {
|
{!tile.photo
|
||||||
sizesAttr: `${this.image_sizes[shape].width}px`,
|
? ""
|
||||||
|
: jdd_context.render_image(tile.photo.image, {
|
||||||
|
sizesAttr:
|
||||||
|
"(max-width: 730px) 100vw, (max-width: 980px) 50vw, 810px",
|
||||||
alt: tile.photo.alt,
|
alt: tile.photo.alt,
|
||||||
container: {
|
container: {
|
||||||
...this.image_sizes[shape],
|
...this.image_sizes[shape],
|
||||||
@ -94,6 +110,7 @@ export class DynamicGrid extends Component<typeof component_arguments> {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -119,14 +136,24 @@ export class DynamicGrid extends Component<typeof component_arguments> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML({
|
toHTML(
|
||||||
|
{
|
||||||
args: { heading, tabs },
|
args: { heading, tabs },
|
||||||
classes,
|
|
||||||
jdd_context,
|
jdd_context,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
classes,
|
||||||
|
index,
|
||||||
|
}: ComponentToHTMLArgs<typeof component_arguments>,
|
||||||
|
tab_style_when_active = `border-bottom-color: #fff;
|
||||||
|
border-top-color: #b721ff;
|
||||||
|
background: #fff;
|
||||||
|
color: #222;`
|
||||||
|
): FlatTemplatable {
|
||||||
const { value: id } = generate_id.next();
|
const { value: id } = generate_id.next();
|
||||||
return (
|
return (
|
||||||
<div class={["dynamic-grid-component", ...classes]}>
|
<div
|
||||||
|
class={["dynamic-grid-component", ...classes]}
|
||||||
|
style={`--jdd-index: ${index}`}
|
||||||
|
>
|
||||||
<h2 class="dynamic-grid-title">{heading}</h2>
|
<h2 class="dynamic-grid-title">{heading}</h2>
|
||||||
<ul class="tabs-menu">
|
<ul class="tabs-menu">
|
||||||
{tabs.map((tab, index) => {
|
{tabs.map((tab, index) => {
|
||||||
@ -137,10 +164,7 @@ export class DynamicGrid extends Component<typeof component_arguments> {
|
|||||||
/* HTML */ `<style>
|
/* HTML */ `<style>
|
||||||
body:has(#input-${id}-${index}:checked)
|
body:has(#input-${id}-${index}:checked)
|
||||||
li:has([for="input-${id}-${index}"]) {
|
li:has([for="input-${id}-${index}"]) {
|
||||||
border-bottom-color: #fff;
|
${tab_style_when_active}
|
||||||
border-top-color: #b721ff;
|
|
||||||
background: #fff;
|
|
||||||
color: #222;
|
|
||||||
}
|
}
|
||||||
</style>`
|
</style>`
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
.horizontal-gallery {
|
||||||
|
--max-image-width: 600px;
|
||||||
|
.horizontal-gallery__top-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 48px;
|
||||||
|
font-family: Exotc350DmBdEU, sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
color: #0d4d69;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-top: 0;
|
||||||
|
@container (max-width: 600px) {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-gallery__buttons,
|
||||||
|
&::before {
|
||||||
|
display: flex;
|
||||||
|
width: 92px;
|
||||||
|
content: " ";
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@container (max-width: 600px) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 600px) {
|
||||||
|
&::before {
|
||||||
|
/* repeated from above, as the above selector somehow does
|
||||||
|
not apply to the :before pseudo-element */
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-scroller__element-container {
|
||||||
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
height: 0;
|
||||||
|
background: transparent; /* make scrollbar transparent */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-scroller__element {
|
||||||
|
max-width: 100cqw;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&,
|
||||||
|
picture,
|
||||||
|
img {
|
||||||
|
border-radius: 10px;
|
||||||
|
max-height: min(75vh, 700px);
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100cqw;
|
||||||
|
}
|
||||||
|
|
||||||
|
picture {
|
||||||
|
height: auto;
|
||||||
|
width: 100% !important;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import type { FlatTemplatable } from "tempstream";
|
||||||
|
import { TempstreamJSX } from "tempstream";
|
||||||
|
import type {
|
||||||
|
ComponentToHTMLArgs,
|
||||||
|
ExtractStructuredComponentArgumentsParsed,
|
||||||
|
JDDContext,
|
||||||
|
} from "@sealcode/jdd";
|
||||||
|
import { Component, ComponentArguments, insert_nbsp } from "@sealcode/jdd";
|
||||||
|
import { horizontalScroller } from "../../routes/common/horizontal-scroller/horizontal-scroller.js";
|
||||||
|
|
||||||
|
const component_arguments = {
|
||||||
|
title: new ComponentArguments.ShortText(),
|
||||||
|
images: new ComponentArguments.List(
|
||||||
|
new ComponentArguments.Structured({
|
||||||
|
image: new ComponentArguments.Image(),
|
||||||
|
alt: new ComponentArguments.ShortText(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class HorizontalGallery extends Component<typeof component_arguments> {
|
||||||
|
getArguments() {
|
||||||
|
return component_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle(
|
||||||
|
_: JDDContext,
|
||||||
|
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||||
|
) {
|
||||||
|
return args.title || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toHTML({
|
||||||
|
args: { title, images },
|
||||||
|
classes,
|
||||||
|
jdd_context: { render_image },
|
||||||
|
index,
|
||||||
|
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={["horizontal-gallery", ...classes]}
|
||||||
|
style={`--jdd-index: ${index}`}
|
||||||
|
>
|
||||||
|
{horizontalScroller({
|
||||||
|
elements: (images || []).map((image) =>
|
||||||
|
render_image(image.image, {
|
||||||
|
sizesAttr: "100vw",
|
||||||
|
container: { width: 800, height: 500, objectFit: "contain" },
|
||||||
|
alt: image.alt,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
render: async ({ scroller, markers }) => (
|
||||||
|
<div>
|
||||||
|
<div class="horizontal-gallery__top-bar">
|
||||||
|
<h2>{insert_nbsp(title)}</h2>
|
||||||
|
<div class="horizontal-gallery__buttons">
|
||||||
|
<button
|
||||||
|
class="prev-button"
|
||||||
|
data-action="horizontal-scroller#scrollLeft"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span style="transform: rotate(180deg); margin-right: 16px;">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="next-button"
|
||||||
|
data-action="horizontal-scroller#scrollRight"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{scroller}
|
||||||
|
{markers}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -19,9 +19,11 @@ export class ImageDemo extends Component<typeof component_arguments> {
|
|||||||
toHTML({
|
toHTML({
|
||||||
args: { image_with_alt, multiple_images },
|
args: { image_with_alt, multiple_images },
|
||||||
jdd_context: { render_image },
|
jdd_context: { render_image },
|
||||||
|
classes,
|
||||||
|
index,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||||
return (
|
return (
|
||||||
<div class="image-demo">
|
<div class={["image-demo", ...classes]} style={`--jdd-index: ${index}`}>
|
||||||
<h2>Image with alt text</h2>
|
<h2>Image with alt text</h2>
|
||||||
{render_image(image_with_alt.image, {
|
{render_image(image_with_alt.image, {
|
||||||
container: { width: 200, height: 200 },
|
container: { width: 200, height: 200 },
|
||||||
|
@ -2,20 +2,12 @@ import { TempstreamJSX } from "tempstream";
|
|||||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
|
|
||||||
const coordinates = new ComponentArguments.ShortText();
|
|
||||||
coordinates.setExampleValues([
|
|
||||||
"52.39706859245613, 16.898251247012197",
|
|
||||||
"52.8, 16.6",
|
|
||||||
"52.5, 16.1",
|
|
||||||
"52.1, 16.35",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const component_arguments = {
|
const component_arguments = {
|
||||||
pins: new ComponentArguments.List(
|
pins: new ComponentArguments.List(
|
||||||
new ComponentArguments.Structured({
|
new ComponentArguments.Structured({
|
||||||
title: new ComponentArguments.ShortText(),
|
title: new ComponentArguments.ShortText(),
|
||||||
address: new ComponentArguments.ShortText(),
|
address: new ComponentArguments.ShortText(),
|
||||||
coordinates,
|
coordinates: new ComponentArguments.ShortText(),
|
||||||
button: new ComponentArguments.Structured({
|
button: new ComponentArguments.Structured({
|
||||||
text: new ComponentArguments.ShortText(),
|
text: new ComponentArguments.ShortText(),
|
||||||
link: new ComponentArguments.ShortText(),
|
link: new ComponentArguments.ShortText(),
|
||||||
@ -48,10 +40,15 @@ export class MapWithPins extends Component<typeof component_arguments> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML({ args: { pins } }: ComponentToHTMLArgs<typeof component_arguments>) {
|
toHTML({
|
||||||
|
args: { pins },
|
||||||
|
classes,
|
||||||
|
index,
|
||||||
|
}: ComponentToHTMLArgs<typeof component_arguments>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="map-with-pins"
|
class={["map-with-pins bleed-out", ...classes]}
|
||||||
|
style={`--jdd-index: ${index}`}
|
||||||
data-controller="map-with-pins"
|
data-controller="map-with-pins"
|
||||||
data-map-with-pins-pins-value={JSON.stringify(pins)
|
data-map-with-pins-pins-value={JSON.stringify(pins)
|
||||||
.replaceAll("\n", "\\n")
|
.replaceAll("\n", "\\n")
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Controller } from "stimulus";
|
import { Controller } from "stimulus";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
@ -60,6 +64,7 @@ export default class MapWithPins extends Controller {
|
|||||||
"loaded-https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
|
"loaded-https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
|
||||||
() => {
|
() => {
|
||||||
this.initiateMap();
|
this.initiateMap();
|
||||||
|
(window as any).externalLinks();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -68,17 +73,23 @@ export default class MapWithPins extends Controller {
|
|||||||
disconnect() {
|
disconnect() {
|
||||||
this.map.remove();
|
this.map.remove();
|
||||||
this.initiated = false;
|
this.initiated = false;
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver?.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
initiateMap() {
|
initiateMap() {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
this.map = window.L.map(this.element as HTMLElement);
|
this.map = window.L.map(this.element as HTMLElement, {
|
||||||
|
dragging: "ontouchstart" in document.documentElement,
|
||||||
|
scrollWheelZoom: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.ResizeObserver) {
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
this.map.invalidateSize();
|
this.map.invalidateSize();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.resizeObserver.observe(this.element);
|
this.resizeObserver?.observe(this.element);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
window.L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
window.L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { TempstreamJSX } from "tempstream";
|
import { TempstreamJSX } from "tempstream";
|
||||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
import type {
|
||||||
|
ComponentToHTMLArgs,
|
||||||
|
ExtractStructuredComponentArgumentsParsed,
|
||||||
|
JDDContext,
|
||||||
|
} from "@sealcode/jdd";
|
||||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
|
|
||||||
const component_arguments = {
|
const component_arguments = {
|
||||||
@ -18,13 +22,21 @@ export class NiceBox extends Component<typeof component_arguments> {
|
|||||||
return component_arguments;
|
return component_arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTitle(
|
||||||
|
_: JDDContext,
|
||||||
|
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||||
|
) {
|
||||||
|
return args.title || null;
|
||||||
|
}
|
||||||
|
|
||||||
async toHTML({
|
async toHTML({
|
||||||
args: { title, content, images },
|
args: { title, content, images },
|
||||||
classes,
|
classes,
|
||||||
jdd_context: { render_markdown, render_image, language },
|
jdd_context: { render_markdown, render_image, language },
|
||||||
|
index,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||||
return (
|
return (
|
||||||
<div class={["nice-box", ...classes]}>
|
<div class={["nice-box", ...classes]} style={`--jdd-index: ${index}`}>
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
<div>{render_markdown(language, content)}</div>
|
<div>{render_markdown(language, content)}</div>
|
||||||
{images.map((image) =>
|
{images.map((image) =>
|
||||||
|
9
src/back/jdd-components/photo/photo.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.image-jdd {
|
||||||
|
picture {
|
||||||
|
border-radius: 10px;
|
||||||
|
img {
|
||||||
|
width: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/back/jdd-components/photo/photo.jdd.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type { FlatTemplatable } from "tempstream";
|
||||||
|
import { TempstreamJSX } from "tempstream";
|
||||||
|
import type {
|
||||||
|
ComponentToHTMLArgs,
|
||||||
|
ExtractStructuredComponentArgumentsParsed,
|
||||||
|
JDDContext,
|
||||||
|
} from "@sealcode/jdd";
|
||||||
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
|
|
||||||
|
import _locreq from "locreq";
|
||||||
|
|
||||||
|
const locreq = _locreq(new URL("./", import.meta.url).pathname);
|
||||||
|
|
||||||
|
const component_arguments = {
|
||||||
|
image: new ComponentArguments.Structured({
|
||||||
|
image: new ComponentArguments.Image().setExampleImages(
|
||||||
|
["image1.jpg", "image2.jpg", "image3.jpg", "image5.jpg"].map((name) =>
|
||||||
|
locreq.resolve("assets/" + name)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
alt: new ComponentArguments.ShortText(),
|
||||||
|
}),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class Photo extends Component<typeof component_arguments> {
|
||||||
|
getArguments() {
|
||||||
|
return component_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle(
|
||||||
|
_: JDDContext,
|
||||||
|
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||||
|
) {
|
||||||
|
return args.image.alt || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeadings() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
toHTML({
|
||||||
|
args: { image },
|
||||||
|
classes,
|
||||||
|
jdd_context: { render_image },
|
||||||
|
index,
|
||||||
|
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||||
|
return (
|
||||||
|
<div class={["image-jdd", ...classes]} style={`--jdd-index: ${index}`}>
|
||||||
|
{render_image(image.image, {
|
||||||
|
sizesAttr: "100vw",
|
||||||
|
alt: image.alt,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,13 +6,7 @@ const component_arguments = {
|
|||||||
table: new ComponentArguments.Table(
|
table: new ComponentArguments.Table(
|
||||||
new ComponentArguments.ShortText(),
|
new ComponentArguments.ShortText(),
|
||||||
new ComponentArguments.Structured({
|
new ComponentArguments.Structured({
|
||||||
color: new ComponentArguments.Enum(["red", "blue", "green", "aquamarine"]),
|
content: new ComponentArguments.ShortText().setExampleValues([""]),
|
||||||
word: new ComponentArguments.ShortText().setExampleValues([
|
|
||||||
"apple",
|
|
||||||
"banana",
|
|
||||||
"pineapple",
|
|
||||||
"carrot",
|
|
||||||
]),
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
} as const;
|
} as const;
|
||||||
@ -22,9 +16,13 @@ export class Table extends Component<typeof component_arguments> {
|
|||||||
return component_arguments;
|
return component_arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML({ args: { table } }: ComponentToHTMLArgs<typeof component_arguments>) {
|
toHTML({
|
||||||
|
args: { table },
|
||||||
|
classes,
|
||||||
|
index,
|
||||||
|
}: ComponentToHTMLArgs<typeof component_arguments>) {
|
||||||
return (
|
return (
|
||||||
<div class="table">
|
<div class={["table", ...classes]} style={`--jdd-index: ${index}`}>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.rows.map((row) =>
|
{table.rows.map((row) =>
|
||||||
@ -40,8 +38,8 @@ export class Table extends Component<typeof component_arguments> {
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
{row.cells.map(({ color, word }) => (
|
{row.cells.map(({ content }) => (
|
||||||
<td style={`color: ${color}`}>{word}</td>
|
<td style>{content}</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
6
src/back/jdd-components/tekst/tekst.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.tekst.markdown {
|
||||||
|
font-family: Poppins, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #0d4d69;
|
||||||
|
}
|
47
src/back/jdd-components/tekst/tekst.jdd.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type { FlatTemplatable } from "tempstream";
|
||||||
|
import { TempstreamJSX } from "tempstream";
|
||||||
|
import type {
|
||||||
|
ComponentToHTMLArgs,
|
||||||
|
ExtractStructuredComponentArgumentsParsed,
|
||||||
|
JDDContext,
|
||||||
|
} from "@sealcode/jdd";
|
||||||
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
|
|
||||||
|
const component_arguments = {
|
||||||
|
content: new ComponentArguments.Markdown(),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class Tekst extends Component<typeof component_arguments> {
|
||||||
|
getArguments() {
|
||||||
|
return component_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle(
|
||||||
|
_: JDDContext,
|
||||||
|
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||||
|
) {
|
||||||
|
return args.content.slice(0, 20) + "..." || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toHTML({
|
||||||
|
args: { content },
|
||||||
|
classes,
|
||||||
|
jdd_context: { render_markdown, language },
|
||||||
|
index,
|
||||||
|
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||||
|
return (
|
||||||
|
<div class={["tekst markdown", ...classes]} style={`--jdd-index: ${index}`}>
|
||||||
|
{render_markdown(language, content)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeadings(
|
||||||
|
context: JDDContext,
|
||||||
|
args: ExtractStructuredComponentArgumentsParsed<{
|
||||||
|
readonly content: ComponentArguments.Markdown;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
return this.getArguments().content.extractHeadings(args.content);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
import type { BaseContext } from "koa";
|
import type { Context } from "koa";
|
||||||
import type { JDDContext } from "@sealcode/jdd";
|
import type { JDDContext } from "@sealcode/jdd";
|
||||||
import type { FilePointer } from "@sealcode/file-manager";
|
import type { FilePointer } from "@sealcode/file-manager";
|
||||||
import { makeSimpleJDDContext } from "@sealcode/jdd";
|
import { makeSimpleJDDContext } from "@sealcode/jdd";
|
||||||
import { TheFileManager } from "./file-manager.js";
|
import { TheFileManager } from "./file-manager.js";
|
||||||
import { imageRouter } from "./image-router.js";
|
import { imageRouter } from "./image-router.js";
|
||||||
|
|
||||||
export function makeJDDContext(ctx: BaseContext): JDDContext {
|
export function makeJDDContext(ctx: Context): JDDContext {
|
||||||
return {
|
return {
|
||||||
language: "pl",
|
language: "pl",
|
||||||
...makeSimpleJDDContext(TheFileManager),
|
...makeSimpleJDDContext(TheFileManager),
|
||||||
|
5
src/back/main.admin.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
table td .list-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { LONG_TEST_TIMEOUT } from "../test_utils/webhint.js";
|
||||||
import { withProdApp } from "../test_utils/with-prod-app.js";
|
import { withProdApp } from "../test_utils/with-prod-app.js";
|
||||||
|
|
||||||
describe("roles", () => {
|
describe("roles", () => {
|
||||||
@ -16,9 +17,10 @@ describe("roles", () => {
|
|||||||
email: "admin@example.com",
|
email: "admin@example.com",
|
||||||
roles: [],
|
roles: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.collections["user-roles"].suCreate({
|
await app.collections["user-roles"].suCreate({
|
||||||
user: admin.id,
|
user: admin.id,
|
||||||
role: "admin",
|
role: "admin",
|
||||||
});
|
});
|
||||||
}));
|
})).timeout(LONG_TEST_TIMEOUT);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import type { Context } from "sealious";
|
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||||
|
import type { ActionName, Collection, Context } from "sealious";
|
||||||
|
import { Policies } from "sealious";
|
||||||
import { Policy, QueryTypes } from "sealious";
|
import { Policy, QueryTypes } from "sealious";
|
||||||
|
|
||||||
export class Roles extends Policy {
|
export class Roles extends Policy {
|
||||||
@ -12,17 +14,19 @@ export class Roles extends Policy {
|
|||||||
async countMatchingRoles(context: Context) {
|
async countMatchingRoles(context: Context) {
|
||||||
const user_id = context.user_id;
|
const user_id = context.user_id;
|
||||||
context.app.Logger.debug2("ROLES", "Checking the roles for user", user_id);
|
context.app.Logger.debug2("ROLES", "Checking the roles for user", user_id);
|
||||||
if (!context.app.collections["user-roles"]) {
|
const roles = await context.cache("roles_for_this_user", async () => {
|
||||||
throw new Error(`context.app.collections["user-roles"] undefined`);
|
const { items: user_roles } = await (
|
||||||
}
|
context.app.collections["user-roles"] as Collection
|
||||||
const user_roles = await context.app.collections["user-roles"]
|
)
|
||||||
.list(context)
|
.suList()
|
||||||
.filter({ user: user_id })
|
.filter({ user: user_id })
|
||||||
.fetch();
|
.fetch();
|
||||||
const roles = user_roles.items.map(
|
const roles = user_roles.map(
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
(user_role) => user_role.get("role") as string
|
(user_role) => user_role.get("role") as string
|
||||||
);
|
);
|
||||||
|
return roles;
|
||||||
|
});
|
||||||
|
|
||||||
return this.allowed_roles.filter((allowed_role) => roles.includes(allowed_role))
|
return this.allowed_roles.filter((allowed_role) => roles.includes(allowed_role))
|
||||||
.length;
|
.length;
|
||||||
@ -60,3 +64,27 @@ export class Roles extends Policy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function CRUDRoles(
|
||||||
|
collection_names: string[],
|
||||||
|
fallbacks: Partial<Record<ActionName, Policy>> = {}
|
||||||
|
) {
|
||||||
|
const result = Object.fromEntries(
|
||||||
|
(["show", "create", "edit", "delete"] as ActionName[]).map((action_name) => {
|
||||||
|
const crud_policy = new Roles([
|
||||||
|
"admin",
|
||||||
|
...collection_names.map(
|
||||||
|
(collection_name) => collection_name + "_" + action_name
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
const fallback_policy = fallbacks[action_name];
|
||||||
|
return [
|
||||||
|
action_name,
|
||||||
|
fallback_policy
|
||||||
|
? new Policies.Or([crud_policy, fallback_policy])
|
||||||
|
: crud_policy,
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// console.log(collection_names, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { withProdApp } from "../test_utils/with-prod-app.js";
|
import { withProdApp } from "../test_utils/with-prod-app.js";
|
||||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
|
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
|
||||||
import { TabsDemoURL } from "./urls.js";
|
import { HomeURL } from "./urls.js";
|
||||||
import { getBrowser } from "../test_utils/browser-creator.js";
|
import { getBrowser } from "../test_utils/browser-creator.js";
|
||||||
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
|
||||||
describe("TabsDemo webhint", () => {
|
describe("Home webhint", () => {
|
||||||
it("doesn't crash", async function () {
|
it("doesn't crash", async function () {
|
||||||
return withProdApp(async ({ base_url, rest_api }) => {
|
return withProdApp(async ({ base_url, rest_api }) => {
|
||||||
await rest_api.get(TabsDemoURL);
|
await rest_api.get(HomeURL);
|
||||||
await webhintURL(base_url + TabsDemoURL);
|
await webhintURL(base_url + HomeURL);
|
||||||
// alternatively you can use webhintHTML for faster but less precise scans
|
// alternatively you can use webhintHTML for faster but less precise scans
|
||||||
// or for scanning responses of requests that use some form of authorization:
|
// or for scanning responses of requests that use some form of authorization:
|
||||||
// const response = await rest_api.get(TabsDemoURL);
|
// const response = await rest_api.get(HomeURL);
|
||||||
// await webhintHTML(response);
|
// await webhintHTML(response);
|
||||||
});
|
});
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("TabsDemo", () => {
|
describe("Home", () => {
|
||||||
let page: Page;
|
let page: Page;
|
||||||
let browser: Browser;
|
let browser: Browser;
|
||||||
let context: BrowserContext;
|
let context: BrowserContext;
|
||||||
@ -34,7 +34,7 @@ describe("TabsDemo", () => {
|
|||||||
|
|
||||||
it("works as expected", async function () {
|
it("works as expected", async function () {
|
||||||
return withProdApp(async ({ base_url }) => {
|
return withProdApp(async ({ base_url }) => {
|
||||||
await page.goto(base_url + TabsDemoURL);
|
await page.goto(base_url + HomeURL);
|
||||||
});
|
});
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
});
|
});
|
@ -1,46 +0,0 @@
|
|||||||
import type { Context } from "koa";
|
|
||||||
import { tempstream, TempstreamJSX } from "tempstream";
|
|
||||||
import { Page } from "@sealcode/sealgen";
|
|
||||||
import html, { defaultHead } from "../html.js";
|
|
||||||
import { registry } from "../jdd-components/components.js";
|
|
||||||
import { documentContainerFromParsed, render, renderEarlyAssets } from "@sealcode/jdd";
|
|
||||||
import { shuffle } from "../util.js";
|
|
||||||
import { makeJDDContext } from "../jdd-context.js";
|
|
||||||
|
|
||||||
export const actionName = "AllComponents";
|
|
||||||
|
|
||||||
export default new (class AllComponentsPage extends Page {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
async canAccess(_: Context) {
|
|
||||||
return { canAccess: true, message: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
async render(ctx: Context) {
|
|
||||||
const components = registry.getAll();
|
|
||||||
const jdd_context = makeJDDContext(ctx);
|
|
||||||
const document = await Promise.all(
|
|
||||||
shuffle(Object.entries(components)).map(
|
|
||||||
async ([component_name, component]) => {
|
|
||||||
return {
|
|
||||||
component_name,
|
|
||||||
args: await component.getExampleValues(jdd_context),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return html(
|
|
||||||
ctx,
|
|
||||||
"AllComponents",
|
|
||||||
<div style="max-width: 1024px; margin: 0 auto; container-type: inline-size;">
|
|
||||||
{render(registry, documentContainerFromParsed(document), jdd_context)}
|
|
||||||
</div>,
|
|
||||||
{},
|
|
||||||
(...args) =>
|
|
||||||
tempstream`${defaultHead(...args)}${renderEarlyAssets(
|
|
||||||
registry,
|
|
||||||
documentContainerFromParsed(document),
|
|
||||||
jdd_context
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})();
|
|
@ -1,40 +0,0 @@
|
|||||||
import { withProdApp } from "../test_utils/with-prod-app.js";
|
|
||||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
|
|
||||||
import { AllComponentsURL } from "./urls.js";
|
|
||||||
import { getBrowser } from "../test_utils/browser-creator.js";
|
|
||||||
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
|
||||||
|
|
||||||
describe("AllComponents webhint", () => {
|
|
||||||
it("doesn't crash", async function () {
|
|
||||||
return withProdApp(async ({ base_url, rest_api }) => {
|
|
||||||
await rest_api.get(AllComponentsURL);
|
|
||||||
await webhintURL(base_url + AllComponentsURL);
|
|
||||||
// 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(AllComponentsURL);
|
|
||||||
// await webhintHTML(response);
|
|
||||||
});
|
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("AllComponents", () => {
|
|
||||||
let page: Page;
|
|
||||||
let browser: Browser;
|
|
||||||
let context: BrowserContext;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
browser = await getBrowser();
|
|
||||||
context = await browser.newContext();
|
|
||||||
page = await context.newPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await context.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("works as expected", async function () {
|
|
||||||
return withProdApp(async ({ base_url }) => {
|
|
||||||
await page.goto(base_url + AllComponentsURL);
|
|
||||||
});
|
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
|
||||||
});
|
|
@ -63,7 +63,7 @@ export function fullscreenMenu({
|
|||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
{Object.keys(panels).map((panel_id) => (
|
{Object.keys(panels).map((panel_id) => (
|
||||||
<span>
|
<>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name={`${menu_id}__active_panel`}
|
name={`${menu_id}__active_panel`}
|
||||||
@ -79,7 +79,7 @@ export function fullscreenMenu({
|
|||||||
${full_styles.panel_open}
|
${full_styles.panel_open}
|
||||||
}
|
}
|
||||||
</style>`}
|
</style>`}
|
||||||
</span>
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="fullscreen-menu__content">
|
<div class="fullscreen-menu__content">
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import type { FlatTemplatable } from "tempstream";
|
import type { FlatTemplatable } from "tempstream";
|
||||||
import { tempstream, TempstreamJSX } from "tempstream";
|
import { tempstream, TempstreamJSX } from "tempstream";
|
||||||
|
import type { Stringifiable } from "tempstream/@types/stringify.js";
|
||||||
|
|
||||||
const make_id = function* () {
|
const make_id = (function* () {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
yield i++;
|
yield i++;
|
||||||
@ -9,7 +10,7 @@ const make_id = function* () {
|
|||||||
i = 0;
|
i = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|
||||||
export async function horizontalScroller({
|
export async function horizontalScroller({
|
||||||
classes = [],
|
classes = [],
|
||||||
@ -17,13 +18,13 @@ export async function horizontalScroller({
|
|||||||
render = async ({ scroller, markers }) => tempstream`${scroller}${markers}`,
|
render = async ({ scroller, markers }) => tempstream`${scroller}${markers}`,
|
||||||
}: {
|
}: {
|
||||||
classes?: string[];
|
classes?: string[];
|
||||||
elements: FlatTemplatable[];
|
elements: (Stringifiable | Promise<Stringifiable>)[];
|
||||||
render?: (options: {
|
render?: (options: {
|
||||||
scroller: FlatTemplatable;
|
scroller: FlatTemplatable;
|
||||||
markers: FlatTemplatable;
|
markers: FlatTemplatable;
|
||||||
}) => Promise<FlatTemplatable>;
|
}) => Promise<FlatTemplatable>;
|
||||||
}) {
|
}) {
|
||||||
const id = make_id().next().value;
|
const id = make_id.next().value;
|
||||||
const scroller_id = `horizontal-scroller-${id}`;
|
const scroller_id = `horizontal-scroller-${id}`;
|
||||||
const scroller = (
|
const scroller = (
|
||||||
<div
|
<div
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import html from "../../html.js";
|
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { Readable } from "stream";
|
|
||||||
import { tempstream } from "tempstream";
|
|
||||||
|
|
||||||
export function MainView(ctx: BaseContext): Readable {
|
|
||||||
return html(
|
|
||||||
ctx,
|
|
||||||
"",
|
|
||||||
tempstream/* HTML */ `
|
|
||||||
<title>My Own ToDo App</title>
|
|
||||||
|
|
||||||
<h1>Sealious App</h1>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,19 +1,13 @@
|
|||||||
import type { BaseContext } from "koa";
|
import type { BaseContext } from "koa";
|
||||||
import type { FlatTemplatable } from "tempstream";
|
import type { FlatTemplatable } from "tempstream";
|
||||||
import { SignUpURL, SignInURL, TodoURL, LogoutURL } from "../urls.js";
|
import { SignInURL, LogoutURL } from "../urls.js";
|
||||||
|
|
||||||
export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable> {
|
export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable> {
|
||||||
const isLoggedIn = !!ctx.$context.session_id;
|
const isLoggedIn = !!ctx.$context.session_id;
|
||||||
|
|
||||||
const linkData = isLoggedIn
|
const linkData = isLoggedIn
|
||||||
? [
|
? [{ text: "Logout", url: LogoutURL }]
|
||||||
{ text: "Logout", url: LogoutURL },
|
: [{ text: "Sign in", url: SignInURL }];
|
||||||
{ text: "To do app", url: TodoURL },
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{ text: "Sign in", url: SignInURL },
|
|
||||||
{ text: "Sign up", url: SignUpURL },
|
|
||||||
];
|
|
||||||
|
|
||||||
const linksHTML = linkData
|
const linksHTML = linkData
|
||||||
.map((link) =>
|
.map((link) =>
|
||||||
|
@ -24,7 +24,7 @@ export function tabs({
|
|||||||
content: FlatTemplatable | Promise<FlatTemplatable>;
|
content: FlatTemplatable | Promise<FlatTemplatable>;
|
||||||
}[];
|
}[];
|
||||||
default_tab: string;
|
default_tab: string;
|
||||||
tab_bar?: FlatTemplatable;
|
tab_bar?: FlatTemplatable | Promise<FlatTemplatable>;
|
||||||
remember_tab?: boolean;
|
remember_tab?: boolean;
|
||||||
active_navbar_tab_style?: string;
|
active_navbar_tab_style?: string;
|
||||||
}) {
|
}) {
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { CollectionItem } from "sealious";
|
|
||||||
import frame from "../../frame.js";
|
|
||||||
import type { Tasks } from "../../collections/collections.js";
|
|
||||||
|
|
||||||
export function Task(task: CollectionItem<typeof Tasks>) {
|
|
||||||
const title = task.get("title");
|
|
||||||
const title_string = title ? title : "";
|
|
||||||
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" : ""}
|
|
||||||
/>
|
|
||||||
${title_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>
|
|
||||||
`;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import { Controller } from "stimulus";
|
|
||||||
|
|
||||||
export default class AutogrowTextarea extends Controller<HTMLTextAreaElement> {
|
|
||||||
connect() {
|
|
||||||
this.autogrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
autogrow() {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
||||||
(this.element.parentNode as any).dataset.replicatedValue = this.element.value;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
|
||||||
import { Controller } from "stimulus";
|
|
||||||
|
|
||||||
export default class ComponentDebugger extends Controller {
|
|
||||||
declare gutterTarget: HTMLDivElement;
|
|
||||||
declare checkboxTarget: HTMLInputElement;
|
|
||||||
declare checkboxTargets: HTMLInputElement[];
|
|
||||||
declare previewTarget: HTMLDivElement;
|
|
||||||
declare componentBlockTargets: HTMLDivElement[];
|
|
||||||
declare componentBlockTargetDisconnected: (e: HTMLDivElement) => void;
|
|
||||||
static targets = ["gutter", "componentBlock", "checkbox", "preview"];
|
|
||||||
|
|
||||||
id: string;
|
|
||||||
main_form: HTMLFormElement;
|
|
||||||
is_resizing = false;
|
|
||||||
origin_x: number;
|
|
||||||
origin_width: number;
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.main_form = document.querySelector("#component-debugger").closest("form");
|
|
||||||
document.documentElement.addEventListener("ts-rebuilt", () => {
|
|
||||||
this.main_form.requestSubmit();
|
|
||||||
});
|
|
||||||
this.main_form.addEventListener("turbo:submit-end", () => {
|
|
||||||
// this clears the values of file inputs, so they don't get unecessarily
|
|
||||||
// re-uploaded on future submissions - the file is alreade there on the server
|
|
||||||
this.main_form
|
|
||||||
.querySelectorAll("input[type=file]")
|
|
||||||
.forEach((input: HTMLInputElement) => (input.value = ""));
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
this.update_width_display();
|
|
||||||
});
|
|
||||||
document.addEventListener("turbo:render", () => this.update_width_display());
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const gutter = this.gutterTarget;
|
|
||||||
gutter.addEventListener("mousedown", (e) => {
|
|
||||||
this.is_resizing = true;
|
|
||||||
this.origin_x = e.clientX;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const resizable = this.targets.find("preview") as HTMLSpanElement;
|
|
||||||
this.origin_width = resizable.getBoundingClientRect().width;
|
|
||||||
const handler = (e: MouseEvent) => this.resizeHandler(e);
|
|
||||||
document.addEventListener("mousemove", handler);
|
|
||||||
document.addEventListener("mouseup", () => {
|
|
||||||
document.removeEventListener("mousemove", handler);
|
|
||||||
});
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
update_width_display() {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const preview = this.targets.find("preview") as HTMLSpanElement;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const component_width_element = this.targets.find(
|
|
||||||
"component-width"
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const component_width = preview.offsetWidth;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
component_width_element.innerHTML = `(width: ${component_width}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
resizeHandler(e: MouseEvent) {
|
|
||||||
const width_offset = this.origin_x - e.clientX;
|
|
||||||
const new_width = Math.max(this.origin_width + width_offset, 1);
|
|
||||||
this.setPreviewWidth(new_width);
|
|
||||||
this.update_width_display();
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreviewWidth(width: number) {
|
|
||||||
document
|
|
||||||
.getElementById("component-debugger")
|
|
||||||
.style.setProperty("--resizable-column-width", width.toString() + "px");
|
|
||||||
this.update_width_display();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWidthDropdown() {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const dropdown = this.targets.find("size-select") as HTMLSelectElement;
|
|
||||||
const value = dropdown.value;
|
|
||||||
this.setPreviewWidth(parseInt(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentBlockTargetConnected(block_element: HTMLDivElement) {
|
|
||||||
const index = parseInt(block_element.getAttribute("data-component-index"));
|
|
||||||
block_element.addEventListener("focusin", () => {
|
|
||||||
this.scrollToComponentPreview(index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
previewTargetConnected(preview_element: HTMLDivElement) {
|
|
||||||
preview_element.addEventListener("click", ({ target }) => {
|
|
||||||
if (!(target instanceof HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const closest = target.closest(".jdd-component");
|
|
||||||
if (!closest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = parseInt(
|
|
||||||
Array.from(closest.classList)
|
|
||||||
.find((c) => c.startsWith("component-number-"))
|
|
||||||
?.replace("component-number-", "")
|
|
||||||
);
|
|
||||||
if (isNaN(index)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.focusComponentBlock(index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
focusComponentBlock(index: number) {
|
|
||||||
const block = this.componentBlockTargets[index];
|
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.checkboxTargets[index].checked = true;
|
|
||||||
block.scrollIntoView({ behavior: "smooth" });
|
|
||||||
(
|
|
||||||
block.querySelector(".component-preview-parameters input") as HTMLInputElement
|
|
||||||
)?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
getIndex(block_element: HTMLDivElement) {
|
|
||||||
const index = parseInt(block_element.getAttribute("data-component-index"));
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
labelClicked(element: MouseEvent) {
|
|
||||||
const block_element = (element.target as HTMLDivElement).closest(
|
|
||||||
`[data-component-debugger-target="componentBlock"]`
|
|
||||||
) as HTMLDivElement;
|
|
||||||
const index = this.getIndex(block_element);
|
|
||||||
if (!this.checkboxTargets?.[index].checked) {
|
|
||||||
this.scrollToComponentPreview(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPreviewElementForComponentIndex(index: number) {
|
|
||||||
const element = this.element.querySelector(
|
|
||||||
`.component-number-${index}`
|
|
||||||
) as HTMLDialogElement;
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToComponentPreview(index: number) {
|
|
||||||
const element = this.getPreviewElementForComponentIndex(index);
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const preview_element = this.element.querySelector(".component-preview");
|
|
||||||
if (element.clientHeight > preview_element.clientHeight) {
|
|
||||||
preview_element.scrollTop = element.offsetTop - 44;
|
|
||||||
} else {
|
|
||||||
preview_element.scrollTop =
|
|
||||||
element.offsetTop -
|
|
||||||
(preview_element.clientHeight - element.clientHeight) / 2 -
|
|
||||||
44;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import type { Enum } from "@sealcode/jdd";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import { printArgPath } from "./print-arg-path.js";
|
|
||||||
|
|
||||||
export function ComponentInputEnum<State, S extends string, T extends Enum<S>>({
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
value,
|
|
||||||
onchange,
|
|
||||||
}: {
|
|
||||||
state: State;
|
|
||||||
arg_path: string[];
|
|
||||||
arg: T;
|
|
||||||
value: string;
|
|
||||||
onchange?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
{arg_path.at(-1) || ""}
|
|
||||||
<select name={`$${printArgPath(arg_path)}`} onchange={onchange}>
|
|
||||||
{arg.values.map((v: S) => (
|
|
||||||
<option value={v} selected={value == v}>
|
|
||||||
{v}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
import type { BaseContext } from "koa";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import type { FilePointer } from "@sealcode/file-manager";
|
|
||||||
import type { Image } from "@sealcode/jdd";
|
|
||||||
import type { StatefulPage } from "@sealcode/sealgen";
|
|
||||||
import type { ComponentPreviewActions } from "./component-preview-actions.js";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
import { makeJDDContext } from "../../jdd-context.js";
|
|
||||||
import { printArgPath } from "./print-arg-path.js";
|
|
||||||
import { htmlEscape } from "escape-goat";
|
|
||||||
|
|
||||||
export function ComponentInputImage<State extends JDDPageState>({
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
value,
|
|
||||||
ctx,
|
|
||||||
page,
|
|
||||||
state,
|
|
||||||
}: {
|
|
||||||
state: State;
|
|
||||||
arg_path: string[];
|
|
||||||
arg: Image;
|
|
||||||
value: FilePointer | null;
|
|
||||||
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
|
|
||||||
ctx: BaseContext;
|
|
||||||
}): JSX.Element {
|
|
||||||
const jdd_context = makeJDDContext(ctx);
|
|
||||||
return (
|
|
||||||
<div style="margin-bottom: 10px">
|
|
||||||
<label
|
|
||||||
style="display: flex; align-items: center; column-gap: 10px;"
|
|
||||||
data-controller="input-image-preview"
|
|
||||||
>
|
|
||||||
{arg_path.at(-1) || ""}
|
|
||||||
<div class="image-preview-container">
|
|
||||||
{value &&
|
|
||||||
jdd_context.render_image(value, {
|
|
||||||
container: { width: 40, height: 40, objectFit: "cover" },
|
|
||||||
crop: { width: 40, height: 40 },
|
|
||||||
style: "height: 40px; width: 40px;",
|
|
||||||
alt: "",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
name={`$${printArgPath(arg_path)}.new`}
|
|
||||||
value=""
|
|
||||||
autocomplete="off"
|
|
||||||
data-action="change->input-image-preview#handleChange"
|
|
||||||
multiple={arg.hasParent("list")}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name={`$${printArgPath(arg_path)}.old`}
|
|
||||||
value={htmlEscape(value?.token || "")}
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "remove_file",
|
|
||||||
label: "❌",
|
|
||||||
},
|
|
||||||
arg_path
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import type { BaseContext } from "koa";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import type { ComponentArgument, List } from "@sealcode/jdd";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
import type { StatefulPage } from "@sealcode/sealgen";
|
|
||||||
import { ComponentInput } from "./component-input.js";
|
|
||||||
import type { ComponentPreviewActions } from "./component-preview-actions.js";
|
|
||||||
|
|
||||||
export async function ComponentInputList<
|
|
||||||
State extends JDDPageState,
|
|
||||||
T extends ComponentArgument<unknown>
|
|
||||||
>({
|
|
||||||
state,
|
|
||||||
ctx,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
value,
|
|
||||||
page,
|
|
||||||
}: {
|
|
||||||
state: State;
|
|
||||||
ctx: BaseContext;
|
|
||||||
arg_path: string[];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
arg: List<T, any>;
|
|
||||||
value: T[];
|
|
||||||
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
|
|
||||||
}): Promise<string> {
|
|
||||||
if (!value) {
|
|
||||||
value = [];
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<fieldset>
|
|
||||||
<legend>{arg_path.at(-1)}</legend>
|
|
||||||
{value.map((value, i, all_values) => (
|
|
||||||
<div style="display: flex">
|
|
||||||
<ComponentInput
|
|
||||||
{...{
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path: [...arg_path, i.toString()],
|
|
||||||
arg: arg.item_type,
|
|
||||||
value,
|
|
||||||
page,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "move_array_item_down",
|
|
||||||
label: "↓",
|
|
||||||
disabled: i == all_values.length - 1,
|
|
||||||
},
|
|
||||||
arg_path,
|
|
||||||
i
|
|
||||||
)}
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{ action: "move_array_item_up", label: "↑", disabled: i == 0 },
|
|
||||||
arg_path,
|
|
||||||
i
|
|
||||||
)}
|
|
||||||
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{ action: "remove_array_item", label: "❌" },
|
|
||||||
arg_path,
|
|
||||||
i
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "add_array_item",
|
|
||||||
label: "➕",
|
|
||||||
},
|
|
||||||
arg_path
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { SingleReference } from "@sealcode/jdd";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import { makeJDDContext } from "../../jdd-context.js";
|
|
||||||
import { printArgPath } from "./print-arg-path.js";
|
|
||||||
|
|
||||||
export async function ComponentInputSingleReference<
|
|
||||||
State,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
T extends SingleReference<any>
|
|
||||||
>({
|
|
||||||
ctx,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
value,
|
|
||||||
onchange,
|
|
||||||
}: {
|
|
||||||
ctx: BaseContext;
|
|
||||||
state: State;
|
|
||||||
arg_path: string[];
|
|
||||||
arg: T;
|
|
||||||
value: string;
|
|
||||||
onchange?: string;
|
|
||||||
}): Promise<string> {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
{arg_path.at(-1) || ""}
|
|
||||||
<select name={`$${printArgPath(arg_path)}`} onchange={onchange}>
|
|
||||||
{(await arg.getValues(makeJDDContext(ctx))).map((v) => (
|
|
||||||
<option value={v.value} selected={value == v.value}>
|
|
||||||
{v.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { ComponentArgument, Structured } from "@sealcode/jdd";
|
|
||||||
import type { StatefulPage } from "@sealcode/sealgen";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import { ComponentInput } from "./component-input.js";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
import type { ComponentPreviewActions } from "./component-preview-actions.js";
|
|
||||||
|
|
||||||
export function ComponentInputStructured<
|
|
||||||
T extends Structured<Record<string, ComponentArgument<unknown>>>
|
|
||||||
>({
|
|
||||||
state,
|
|
||||||
ctx,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
value,
|
|
||||||
rerender_callback,
|
|
||||||
page,
|
|
||||||
}: {
|
|
||||||
state: JDDPageState;
|
|
||||||
ctx: BaseContext;
|
|
||||||
arg_path: string[];
|
|
||||||
arg: T;
|
|
||||||
value: Record<string, unknown>;
|
|
||||||
rerender_callback?: string;
|
|
||||||
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<fieldset>
|
|
||||||
<legend>{arg_path.at(-1)}</legend>
|
|
||||||
{Object.entries(arg.structure).map(([arg_name, arg]) => (
|
|
||||||
<div>
|
|
||||||
<ComponentInput
|
|
||||||
{...{
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path: [...arg_path, arg_name],
|
|
||||||
arg,
|
|
||||||
value: value[arg_name],
|
|
||||||
rerender_callback,
|
|
||||||
page,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,225 +0,0 @@
|
|||||||
import type { Table, TableData } from "@sealcode/jdd";
|
|
||||||
import { isTableHeader } from "@sealcode/jdd";
|
|
||||||
import type { StatefulPage } from "@sealcode/sealgen";
|
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import { makeJDDContext } from "../../jdd-context.js";
|
|
||||||
import { ComponentInput } from "./component-input.js";
|
|
||||||
import type { ComponentPreviewActions } from "./component-preview-actions.js";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
import add_column_right_icon from "./table-add-column-right.svg";
|
|
||||||
import add_row_below_icon from "./table-add-row-below.svg";
|
|
||||||
import add_column_header_icon from "./table-add-row-header-below.svg";
|
|
||||||
import delete_column_icon from "./table-delete-column.svg";
|
|
||||||
import delete_row_icon from "./table-delete-row.svg";
|
|
||||||
import move_column_right_icon from "./table-move-column-right.svg";
|
|
||||||
import move_row_down_icon from "./table-move-row-down.svg";
|
|
||||||
|
|
||||||
export async function ComponentInputTable<
|
|
||||||
State extends JDDPageState,
|
|
||||||
CellType,
|
|
||||||
HeaderType
|
|
||||||
>({
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
ctx,
|
|
||||||
arg,
|
|
||||||
value,
|
|
||||||
page,
|
|
||||||
}: {
|
|
||||||
state: State;
|
|
||||||
ctx: BaseContext;
|
|
||||||
arg_path: string[];
|
|
||||||
arg: Table<CellType, HeaderType>;
|
|
||||||
value: TableData<CellType, HeaderType>;
|
|
||||||
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
|
|
||||||
}): Promise<string> {
|
|
||||||
if (!value) {
|
|
||||||
value = await arg.getEmptyValue(makeJDDContext(ctx));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fieldset>
|
|
||||||
<legend>{arg_path.at(-1)}</legend>
|
|
||||||
<div>
|
|
||||||
<table style="position: relative; /* necessary for sticky th*/">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
{[...Array(arg.getColumnsCount(value)).keys()].map(
|
|
||||||
(column_index) => (
|
|
||||||
<th class="sticky sticky--top subdued">
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "remove_table_column",
|
|
||||||
label: "Remove column",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${delete_column_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
arg_path,
|
|
||||||
column_index
|
|
||||||
)}
|
|
||||||
{column_index >= arg.getColumnsCount(value) - 1
|
|
||||||
? ""
|
|
||||||
: page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "move_table_column_right",
|
|
||||||
label: "Move column to the right",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${move_column_right_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
arg_path,
|
|
||||||
column_index
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
{value.rows.map((row, row_index) => (
|
|
||||||
<tr>
|
|
||||||
<td class="sticky sticky--left subdued">
|
|
||||||
<div style="display: flex; flex-flow: column; row-gap: 5px;">
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "remove_table_row",
|
|
||||||
label: "Remove row",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${delete_row_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
arg_path,
|
|
||||||
row_index
|
|
||||||
)}
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "move_table_row_down",
|
|
||||||
label: "Move this row down",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${move_row_down_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
arg_path,
|
|
||||||
row_index
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{isTableHeader(row) ? (
|
|
||||||
<th colspan={arg.getColumnsCount(value).toString()}>
|
|
||||||
<ComponentInput
|
|
||||||
{...{
|
|
||||||
state,
|
|
||||||
ctx,
|
|
||||||
arg_path: [
|
|
||||||
...arg_path,
|
|
||||||
"rows",
|
|
||||||
row_index.toString(),
|
|
||||||
"header_content",
|
|
||||||
],
|
|
||||||
arg: arg.header_type,
|
|
||||||
value: row.header_content,
|
|
||||||
page,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
) : (
|
|
||||||
row.cells.map((cell, cell_index) => (
|
|
||||||
<td>
|
|
||||||
<ComponentInput
|
|
||||||
{...{
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path: [
|
|
||||||
...arg_path,
|
|
||||||
"rows",
|
|
||||||
row_index.toString(),
|
|
||||||
"cells",
|
|
||||||
cell_index.toString(),
|
|
||||||
],
|
|
||||||
arg: arg.cell_type,
|
|
||||||
value: cell,
|
|
||||||
page,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{row_index == 0 ? (
|
|
||||||
<td
|
|
||||||
class="subdued"
|
|
||||||
rowspan={value.rows.length.toString()}
|
|
||||||
>
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "add_table_column",
|
|
||||||
label: "Add column",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${add_column_right_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
arg_path
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
class="subdued"
|
|
||||||
colspan={(arg.getColumnsCount(value) + 1).toString()}
|
|
||||||
>
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "add_table_row",
|
|
||||||
label: "Add table row",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${add_row_below_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
arg_path,
|
|
||||||
arg.getColumnsCount(value)
|
|
||||||
)}
|
|
||||||
{page.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "add_table_row",
|
|
||||||
label: "Add table header",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${add_column_header_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
arg_path,
|
|
||||||
arg.getColumnsCount(value),
|
|
||||||
"header"
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
import { printArgPath } from "./print-arg-path.js";
|
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { ComponentArgument, TableData } from "@sealcode/jdd";
|
|
||||||
import { SingleReference } from "@sealcode/jdd";
|
|
||||||
import { ComponentArguments, Enum, Image, List, Structured, Table } from "@sealcode/jdd";
|
|
||||||
import { ComponentInputStructured } from "./component-input-structured.js";
|
|
||||||
import type { StatefulPage } from "@sealcode/sealgen";
|
|
||||||
import type { ComponentPreviewActions } from "./component-preview-actions.js";
|
|
||||||
import { ComponentInputList } from "./component-input-list.js";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
import { ComponentInputEnum } from "./component-input-enum.js";
|
|
||||||
import { ComponentInputImage } from "./component-input-image.js";
|
|
||||||
import { ComponentInputTable } from "./component-input-table.js";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import type { FilePointer } from "@sealcode/file-manager";
|
|
||||||
import { is, predicates } from "@sealcode/ts-predicates";
|
|
||||||
import { makeJDDContext } from "../../jdd-context.js";
|
|
||||||
import { ComponentInputSingleReference } from "./component-input-single-reference.js";
|
|
||||||
|
|
||||||
export const actionName = "Components";
|
|
||||||
const absoluteUrlPattern = "http(s?)(://)((www.)?)(([^.]+).)?([a-zA-z0-9-_]+)";
|
|
||||||
|
|
||||||
export async function ComponentInput<State extends JDDPageState, T>({
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
value,
|
|
||||||
page,
|
|
||||||
}: {
|
|
||||||
state: State;
|
|
||||||
ctx: BaseContext;
|
|
||||||
arg_path: string[];
|
|
||||||
arg: ComponentArgument<T>;
|
|
||||||
value: T;
|
|
||||||
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
|
|
||||||
}): Promise<string> {
|
|
||||||
if (value === undefined) {
|
|
||||||
value = await arg.getEmptyValue(makeJDDContext(ctx));
|
|
||||||
}
|
|
||||||
if (arg instanceof List) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
return ComponentInputList({
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
value: value as T[],
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const argType = arg.getTypeName();
|
|
||||||
const isUrlAbsolute =
|
|
||||||
arg instanceof ComponentArguments.URL && arg.urlType === "absolute";
|
|
||||||
const inputType = isUrlAbsolute ? "url" : "text";
|
|
||||||
|
|
||||||
if (arg instanceof Structured) {
|
|
||||||
return ComponentInputStructured({
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
value: value as Record<string, unknown>,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg instanceof SingleReference) {
|
|
||||||
return ComponentInputSingleReference({
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
value: value as string,
|
|
||||||
onchange: page.rerender(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg instanceof Enum) {
|
|
||||||
return ComponentInputEnum({
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
value: value as string,
|
|
||||||
onchange: page.rerender(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg instanceof Image) {
|
|
||||||
return ComponentInputImage({
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
value: value as FilePointer,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg instanceof Table) {
|
|
||||||
return ComponentInputTable({
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
arg,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
value: value as TableData<unknown, unknown>,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
{arg_path.at(-1) || ""}
|
|
||||||
{argType == "markdown" ? (
|
|
||||||
<div
|
|
||||||
class="grow-wrap"
|
|
||||||
data-replicated-value={is(value, predicates.string) ? value : ""}
|
|
||||||
>
|
|
||||||
{/* putting the content in the attribute to enable autogrow */}
|
|
||||||
<textarea
|
|
||||||
name={`$${printArgPath(arg_path)}`}
|
|
||||||
onblur={page.rerender()}
|
|
||||||
cols="40"
|
|
||||||
data-controller="autogrow-textarea submit-on-input paste-to-markdown"
|
|
||||||
data-action="autogrow-textarea#autogrow blur->autogrow-textarea#autogrow resize->autogrow-textarea#autogrow submit-on-input#sendValues focus->submit-on-input#makePermanent blur->submit-on-input#makeNotPermanent"
|
|
||||||
>
|
|
||||||
{is(value, predicates.string) ? value : ""}
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
type={inputType}
|
|
||||||
name={`$${printArgPath(arg_path)}`}
|
|
||||||
value={is(value, predicates.string) ? value : ""}
|
|
||||||
size="40"
|
|
||||||
pattern={isUrlAbsolute ? absoluteUrlPattern : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,428 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
||||||
import type { Registry, TableData } from "@sealcode/jdd";
|
|
||||||
import { List, Table } from "@sealcode/jdd";
|
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import { isTableData, isTableRegularRow } from "@sealcode/jdd";
|
|
||||||
import objectPath from "object-path";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
import { registry } from "../../jdd-components/registry.js";
|
|
||||||
import { makeJDDContext } from "../../jdd-context.js";
|
|
||||||
|
|
||||||
function moveElement<T>(array: Array<T>, fromIndex: number, toIndex: number): Array<T> {
|
|
||||||
const element = array.splice(fromIndex, 1)[0];
|
|
||||||
if (!element) {
|
|
||||||
console.error("element is undefined");
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
array.splice(toIndex, 0, element);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getComponentData(
|
|
||||||
state: JDDPageState,
|
|
||||||
arg_path: string[],
|
|
||||||
registry: Registry
|
|
||||||
) {
|
|
||||||
const index_arg = arg_path[1];
|
|
||||||
if (!index_arg) {
|
|
||||||
throw new Error("Missing component index in arg path");
|
|
||||||
}
|
|
||||||
const component_index = parseInt(index_arg);
|
|
||||||
const component_args = state.components[component_index]?.args || {};
|
|
||||||
const component_name = state.components[component_index]?.component_name || "";
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
const arg_path_within_component = arg_path.slice(3); // remove "components" and the index of the component and "args"
|
|
||||||
|
|
||||||
const [argument, , argument_value] = component?.getArgumentAtPath(
|
|
||||||
arg_path_within_component,
|
|
||||||
component_args
|
|
||||||
) || [null, null, null];
|
|
||||||
|
|
||||||
return {
|
|
||||||
component_index,
|
|
||||||
component_args,
|
|
||||||
component_name,
|
|
||||||
component,
|
|
||||||
argument,
|
|
||||||
argument_value,
|
|
||||||
arg_path_within_component,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ComponentPreviewActions = <const>{
|
|
||||||
add_array_item: async (
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[]
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
component_name,
|
|
||||||
component,
|
|
||||||
argument,
|
|
||||||
arg_path_within_component,
|
|
||||||
argument_value,
|
|
||||||
} = getComponentData(state, arg_path, registry);
|
|
||||||
if (!component) {
|
|
||||||
console.error("unknown component: ", component_name);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
if (!argument) {
|
|
||||||
console.error(
|
|
||||||
"Didn't find a list argument at this path",
|
|
||||||
arg_path_within_component
|
|
||||||
);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
if (!(argument instanceof List)) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected argument in path ${arg_path.join(
|
|
||||||
"."
|
|
||||||
)} to be an instance of List`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
objectPath.insert(
|
|
||||||
state,
|
|
||||||
arg_path,
|
|
||||||
await argument.item_type.getExampleValue(makeJDDContext(ctx)),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
Array.isArray(argument_value) ? argument_value.length : 0
|
|
||||||
);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_array_item: (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
index_to_remove: number
|
|
||||||
): JDDPageState => {
|
|
||||||
objectPath.del(state, [...arg_path, index_to_remove]);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
move_array_item_up: async (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_inputs: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
element_index: number
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const array_values = objectPath.get(state, arg_path) as unknown[];
|
|
||||||
const curr = array_values[element_index];
|
|
||||||
const prev = array_values[element_index - 1];
|
|
||||||
if (!prev || !curr) {
|
|
||||||
throw new Error("No element at such index or cannot move it up");
|
|
||||||
}
|
|
||||||
[array_values[element_index - 1], array_values[element_index]] = [curr, prev];
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
move_array_item_down: async (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_inputs: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
element_index: number
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const array_values = objectPath.get(state, arg_path) as unknown[];
|
|
||||||
const curr = array_values[element_index];
|
|
||||||
const next = array_values[element_index + 1];
|
|
||||||
if (!next || !curr) {
|
|
||||||
throw new Error("No element at such index or cannot move it up");
|
|
||||||
}
|
|
||||||
[array_values[element_index], array_values[element_index + 1]] = [next, curr];
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
change_component: async (
|
|
||||||
ctx: BaseContext,
|
|
||||||
_state: JDDPageState,
|
|
||||||
inputs: Record<string, unknown>
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const component_name = inputs.component;
|
|
||||||
if (!component_name || typeof component_name !== "string") {
|
|
||||||
throw new Error(
|
|
||||||
"Missing input: 'component' for action change_component. It should contain the name of the new component type"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
if (!component) {
|
|
||||||
throw new Error(`Unknown or disallowed component name: ${component_name}`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
component_name: component_name,
|
|
||||||
args: (await component?.getExampleValues(makeJDDContext(ctx))) || {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
randomize_args: async (
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_inputs: Record<string, string>,
|
|
||||||
component_index_str: string
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const { component_index, component } = getComponentData(
|
|
||||||
state,
|
|
||||||
["components", component_index_str],
|
|
||||||
registry
|
|
||||||
);
|
|
||||||
|
|
||||||
const component_data = state.components[component_index];
|
|
||||||
if (!component_data) {
|
|
||||||
throw new Error("Missing component data");
|
|
||||||
}
|
|
||||||
component_data.args =
|
|
||||||
(await component?.getExampleValues(makeJDDContext(ctx))) || {};
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
add_table_row: async (
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
columns: number,
|
|
||||||
type: "header" | "row" = "row"
|
|
||||||
) => {
|
|
||||||
const jdd_context = makeJDDContext(ctx);
|
|
||||||
const { component_args, argument } = getComponentData(state, arg_path, registry);
|
|
||||||
let row;
|
|
||||||
|
|
||||||
if (!argument) {
|
|
||||||
console.error("Unknown component at path", arg_path);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
if (!(argument instanceof Table)) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected argument at path ${arg_path.join(".")} to be of type Table`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (type == "header") {
|
|
||||||
row = {
|
|
||||||
type: "header",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
header_content: await argument.header_type.getExampleValue(jdd_context),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const cells = [];
|
|
||||||
for (let i = 0; i < columns; i++) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
cells.push(await argument.cell_type.getExampleValue(jdd_context));
|
|
||||||
}
|
|
||||||
row = { type: "row", cells };
|
|
||||||
}
|
|
||||||
objectPath.insert(
|
|
||||||
state,
|
|
||||||
[...arg_path, "rows"],
|
|
||||||
row,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
((objectPath.get(component_args, [...arg_path, "rows"]) as unknown[]) || [])
|
|
||||||
.length
|
|
||||||
);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
add_table_column: async (
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[]
|
|
||||||
) => {
|
|
||||||
const { argument } = getComponentData(state, arg_path, registry);
|
|
||||||
if (!argument) {
|
|
||||||
console.error("Unknown component at path", arg_path);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
|
|
||||||
if (!isTableData(tableData)) {
|
|
||||||
throw new Error("wrong table data");
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
|
||||||
for (const i in tableData.rows) {
|
|
||||||
const row = tableData.rows[i];
|
|
||||||
if (isTableRegularRow(row)) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
row.cells.push(await argument.getExampleValue(makeJDDContext(ctx)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
objectPath.set(state, arg_path, tableData);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_table_column: (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
column_index_to_remove: number
|
|
||||||
): JDDPageState => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
|
|
||||||
if (!isTableData(tableData)) {
|
|
||||||
throw new Error("wrong table data");
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
|
||||||
for (const i in tableData.rows) {
|
|
||||||
const row = tableData.rows[i];
|
|
||||||
if (isTableRegularRow(row)) {
|
|
||||||
row.cells = row.cells.filter((_, i) => i != column_index_to_remove);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
objectPath.set(state, arg_path, tableData);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_table_row: (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
row_index: number
|
|
||||||
): JDDPageState => {
|
|
||||||
objectPath.del(state, [...arg_path, "rows", row_index]);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
move_table_column_right: (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
column_index: number
|
|
||||||
) => {
|
|
||||||
const { component_args } = getComponentData(state, arg_path, registry);
|
|
||||||
|
|
||||||
const last_path_element = arg_path.at(-1);
|
|
||||||
if (!last_path_element) {
|
|
||||||
throw new Error("arg path is empty");
|
|
||||||
}
|
|
||||||
const data = objectPath.get<unknown>(component_args, last_path_element, "");
|
|
||||||
if (!isTableData(data)) {
|
|
||||||
throw new Error("Expected arg value for a table to be properly shaped");
|
|
||||||
}
|
|
||||||
for (const row of data.rows) {
|
|
||||||
if (row.type == "row") {
|
|
||||||
moveElement(row.cells, column_index, column_index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
objectPath.set(state, [...arg_path, "rows"], data.rows);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
move_table_row_down: (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[],
|
|
||||||
row_index: number
|
|
||||||
) => {
|
|
||||||
const { component_args } = getComponentData(state, arg_path, registry);
|
|
||||||
const last_path_element = arg_path.at(-1);
|
|
||||||
if (!last_path_element) {
|
|
||||||
throw new Error("arg path is empty");
|
|
||||||
}
|
|
||||||
const data = objectPath.get<unknown>(component_args, last_path_element, "");
|
|
||||||
if (!isTableData(data)) {
|
|
||||||
throw new Error("Expected arg value for a table to be properly shaped");
|
|
||||||
}
|
|
||||||
moveElement(data.rows, row_index, row_index + 1);
|
|
||||||
objectPath.set(state, [...arg_path, "rows"], data.rows);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
change_size: (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
inputs: Record<string, string>
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
preview_size: inputs.size,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
add_component: async (
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
inputs: Record<string, string>
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const component_name = inputs.component;
|
|
||||||
if (!component_name) {
|
|
||||||
throw new Error("Missing component name");
|
|
||||||
}
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
components: [
|
|
||||||
...state.components,
|
|
||||||
{
|
|
||||||
component_name: component_name,
|
|
||||||
args: (await component?.getExampleValues(makeJDDContext(ctx))) || {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_component: async (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_inputs: Record<string, string>,
|
|
||||||
component_index: number
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const newComponentState = [...state.components];
|
|
||||||
newComponentState.splice(component_index, 1);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
components: newComponentState,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
move_component_up: async (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_inputs: Record<string, string>,
|
|
||||||
component_index: number
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const newComps = [...state.components];
|
|
||||||
const prev = newComps[component_index - 1];
|
|
||||||
const curr = newComps[component_index];
|
|
||||||
if (!prev || !curr) {
|
|
||||||
throw new Error("No component at such index or cannot move it up");
|
|
||||||
}
|
|
||||||
[newComps[component_index], newComps[component_index - 1]] = [prev, curr];
|
|
||||||
return { ...state, components: newComps };
|
|
||||||
},
|
|
||||||
|
|
||||||
move_component_down: async (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_inputs: Record<string, string>,
|
|
||||||
component_index: number
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
const newComps = [...state.components];
|
|
||||||
const next = newComps[component_index + 1];
|
|
||||||
const curr = newComps[component_index];
|
|
||||||
if (!next || !curr) {
|
|
||||||
throw new Error("No component at such index or cannot move it up");
|
|
||||||
}
|
|
||||||
[newComps[component_index], newComps[component_index + 1]] = [next, curr];
|
|
||||||
return { ...state, components: newComps };
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_file: async (
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[]
|
|
||||||
): Promise<JDDPageState> => {
|
|
||||||
objectPath.set(state, arg_path, null);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,124 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
import type Router from "@koa/router";
|
|
||||||
import type { JDDocumentContainer, RawJDDocument } from "@sealcode/jdd";
|
|
||||||
import {
|
|
||||||
documentContainerFromStorage,
|
|
||||||
documentToParsed,
|
|
||||||
documentToStorage,
|
|
||||||
} from "@sealcode/jdd";
|
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { Collection, CollectionItem, FieldNames } from "sealious";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import { registry } from "../../jdd-components/registry.js";
|
|
||||||
import { makeJDDContext } from "../../jdd-context.js";
|
|
||||||
import JDDCreator from "./jdd-creator.js";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
|
|
||||||
export const actionName = "ArticleContentEdit";
|
|
||||||
|
|
||||||
export abstract class EditJDDField<C extends Collection> extends JDDCreator {
|
|
||||||
async getID(ctx: BaseContext): Promise<string> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
const id = ctx.params["id"] as string;
|
|
||||||
if (!id) {
|
|
||||||
throw new Error("Missing URL parameter: " + "id");
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract getCollection(ctx: BaseContext): C;
|
|
||||||
|
|
||||||
async getItem(ctx: BaseContext): Promise<CollectionItem<C>> {
|
|
||||||
const {
|
|
||||||
items: [item],
|
|
||||||
} = await this.getCollection(ctx)
|
|
||||||
.list(ctx.$context)
|
|
||||||
.ids([await this.getID(ctx)])
|
|
||||||
.fetch();
|
|
||||||
if (!item) {
|
|
||||||
throw new Error("Couldn't get item of id " + (await this.getID(ctx)));
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract getJDDFieldName(): FieldNames<C["fields"]>;
|
|
||||||
|
|
||||||
mount(router: Router, path: string) {
|
|
||||||
super.mount(router, path);
|
|
||||||
|
|
||||||
router.post(path + "save/", async (ctx) => {
|
|
||||||
const { state } = await this.extractState(ctx);
|
|
||||||
const item = await this.getItem(ctx);
|
|
||||||
|
|
||||||
item.set(
|
|
||||||
this.getJDDFieldName(),
|
|
||||||
(
|
|
||||||
await documentToStorage(registry, makeJDDContext(ctx), {
|
|
||||||
value: state.components,
|
|
||||||
} as unknown as JDDocumentContainer<"parsed">)
|
|
||||||
).value as any
|
|
||||||
);
|
|
||||||
await item.save(ctx.$context);
|
|
||||||
ctx.type = "html";
|
|
||||||
ctx.status = 422;
|
|
||||||
if (!state.messages) {
|
|
||||||
state.messages = [];
|
|
||||||
}
|
|
||||||
state.messages.push("Saved!");
|
|
||||||
ctx.body = this.render(ctx, state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderHeader(_ctx: BaseContext, _item: CollectionItem<C>) {
|
|
||||||
return <h1>Edit JDD</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderPreParameterButtons(ctx: BaseContext) {
|
|
||||||
const item = await this.getItem(ctx);
|
|
||||||
return <div>{this.renderHeader(ctx, item)}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderParameterButtons(state: JDDPageState) {
|
|
||||||
{
|
|
||||||
/*The below button has to be here in order for it to be the default behavior */
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input type="submit" value="Preview" />
|
|
||||||
<select name="component">
|
|
||||||
{Object.keys(this.getRegistryComponents()).map((cmp) => (
|
|
||||||
<option value={cmp}>{cmp}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{this.makeActionButton(state, {
|
|
||||||
action: "add_component",
|
|
||||||
label: "Add component",
|
|
||||||
})}
|
|
||||||
<input type="submit" formaction="./save/" value="zapisz" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInitialState(ctx: BaseContext) {
|
|
||||||
const article = await this.getItem(ctx);
|
|
||||||
const parsed_document = await documentToParsed(
|
|
||||||
registry,
|
|
||||||
makeJDDContext(ctx),
|
|
||||||
documentContainerFromStorage(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
(article.get(this.getJDDFieldName()) as RawJDDocument) || []
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
components: parsed_document.value.map((e) => ({ ...e, open: true })),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// uncomment to create whitelist of allowed components
|
|
||||||
// getAllowedComponents() {
|
|
||||||
// return ["nice-box"];
|
|
||||||
// }
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
/* https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ */
|
|
||||||
|
|
||||||
.grow-wrap {
|
|
||||||
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.grow-wrap::after {
|
|
||||||
/* Note the weird space! Needed to preventy jumpy behavior */
|
|
||||||
content: attr(data-replicated-value) " ";
|
|
||||||
|
|
||||||
/* This is how textarea text behaves */
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
/* Hidden from view, clicks, and screen readers */
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.grow-wrap > textarea {
|
|
||||||
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
|
|
||||||
resize: none;
|
|
||||||
|
|
||||||
/* Firefox shows scrollbar on growth, you can hide like this. */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.grow-wrap > textarea,
|
|
||||||
.grow-wrap::after {
|
|
||||||
/* Identical styling required!! */
|
|
||||||
border: 1px solid black;
|
|
||||||
padding: 0.5rem;
|
|
||||||
font: inherit;
|
|
||||||
|
|
||||||
/* Place on top of each other */
|
|
||||||
grid-area: 1 / 1 / 2 / 2;
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { Controller } from "stimulus";
|
|
||||||
|
|
||||||
export default class InputImagePreview extends Controller {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
handleChange() {
|
|
||||||
let img = this.element.querySelector("img");
|
|
||||||
if (!img) {
|
|
||||||
img = document.createElement("img");
|
|
||||||
img.setAttribute("style", "height: 40px; width: 40px");
|
|
||||||
this.element.querySelector(".image-preview-container").appendChild(img);
|
|
||||||
}
|
|
||||||
window.URL.revokeObjectURL(img.src);
|
|
||||||
const new_url = window.URL.createObjectURL(
|
|
||||||
this.element.querySelector("input").files[0]
|
|
||||||
);
|
|
||||||
img.src = new_url;
|
|
||||||
img.parentNode;
|
|
||||||
img.parentElement
|
|
||||||
.querySelectorAll("source")
|
|
||||||
.forEach((e) => e.parentNode.removeChild(e));
|
|
||||||
img.style.aspectRatio = "1";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
import type { BaseContext } from "koa";
|
|
||||||
import { TempstreamJSX } from "tempstream";
|
|
||||||
import { ComponentPreviewActions } from "./component-preview-actions.js";
|
|
||||||
import type { JDDPageState } from "./jdd-page.js";
|
|
||||||
import JDDPage from "./jdd-page.js";
|
|
||||||
|
|
||||||
import move_row_down_icon from "./table-move-row-down.svg";
|
|
||||||
import move_row_up_icon from "./table-move-row-up.svg";
|
|
||||||
|
|
||||||
export default abstract class JDDCreator extends JDDPage {
|
|
||||||
actions = ComponentPreviewActions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method returns list of components allowed in JDD Editor instance.
|
|
||||||
* If list is empty it will allow all of the components in registry,
|
|
||||||
* if you overide this function you can decide on what components should
|
|
||||||
* available.
|
|
||||||
*/
|
|
||||||
getAllowedComponents(): string[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegistryComponents() {
|
|
||||||
const all_components = super.getRegistryComponents();
|
|
||||||
const allowed_components = this.getAllowedComponents();
|
|
||||||
|
|
||||||
if (allowed_components.length > 0) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(all_components).filter(([name]) =>
|
|
||||||
allowed_components.includes(name)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return all_components;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderParameterButtons(state: JDDPageState) {
|
|
||||||
{
|
|
||||||
/*The below button has to be here in order for it to be the default behavior */
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input type="submit" value="Preview" />
|
|
||||||
<select name="component">
|
|
||||||
{Object.keys(this.getRegistryComponents()).map((cmp) => (
|
|
||||||
<option value={cmp}>{cmp}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{this.makeActionButton(state, {
|
|
||||||
action: "add_component",
|
|
||||||
label: "Add component",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderComponentBlock(
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
component: {
|
|
||||||
component_name: string;
|
|
||||||
args: Record<string, unknown>;
|
|
||||||
},
|
|
||||||
component_index: number
|
|
||||||
) {
|
|
||||||
const checkbox_id = `component_${component_index}_open`;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={[
|
|
||||||
"jdd-editor__component-block",
|
|
||||||
"jdd-editor__component-block--number-" + component_index,
|
|
||||||
]}
|
|
||||||
data-component-debugger-target="componentBlock"
|
|
||||||
data-component-index={component_index.toString()}
|
|
||||||
>
|
|
||||||
<summary class="jdd-editor__component-block__top_bar">
|
|
||||||
{this.makeActionButton(
|
|
||||||
state,
|
|
||||||
{ action: "remove_component", label: "❌" },
|
|
||||||
component_index
|
|
||||||
)}
|
|
||||||
{this.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "move_component_up",
|
|
||||||
label: "Move this row up",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${move_row_up_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
component_index
|
|
||||||
)}
|
|
||||||
{this.makeActionButton(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
action: "move_component_down",
|
|
||||||
label: "Move this row down",
|
|
||||||
content: /* HTML */ `<img
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
src="${move_row_down_icon.url}"
|
|
||||||
/>`,
|
|
||||||
},
|
|
||||||
component_index
|
|
||||||
)}
|
|
||||||
<span>{component.component_name}</span>
|
|
||||||
<label
|
|
||||||
class="component-block__handle"
|
|
||||||
for={checkbox_id}
|
|
||||||
style="flex-grow: 1"
|
|
||||||
data-action="click->component-debugger#labelClicked"
|
|
||||||
>
|
|
||||||
<span class="jdd-editor__component-block__chevron"> > </span>
|
|
||||||
</label>
|
|
||||||
</summary>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="component-collapse-toggle"
|
|
||||||
name={`$[components][${component_index}][open]`}
|
|
||||||
data-turbo-permanent
|
|
||||||
id={checkbox_id}
|
|
||||||
style="display:none"
|
|
||||||
data-component-debugger-target="checkbox"
|
|
||||||
/>
|
|
||||||
<div class="jdd-editor__component-block__inner">
|
|
||||||
{super.renderComponentBlock(ctx, state, component, component_index)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,327 +0,0 @@
|
|||||||
import type { Component, RawJDDocument } from "@sealcode/jdd";
|
|
||||||
import { documentContainerFromParsed } from "@sealcode/jdd";
|
|
||||||
import { render, renderEarlyAssets } from "@sealcode/jdd";
|
|
||||||
import { StatefulPage } from "@sealcode/sealgen";
|
|
||||||
import { hasFieldOfType, hasShape, predicates } from "@sealcode/ts-predicates";
|
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { FlatTemplatable, Templatable } from "tempstream";
|
|
||||||
import { tempstream, TempstreamJSX } from "tempstream";
|
|
||||||
import html, { defaultHead } from "../../html.js";
|
|
||||||
import { registry } from "../../jdd-components/components.js";
|
|
||||||
import { makeJDDContext } from "../../jdd-context.js";
|
|
||||||
import { ComponentInput } from "./component-input.js";
|
|
||||||
import { ComponentPreviewActions } from "./component-preview-actions.js";
|
|
||||||
|
|
||||||
export const actionName = "Components";
|
|
||||||
|
|
||||||
export type JDDPageState = {
|
|
||||||
components: RawJDDocument;
|
|
||||||
preview_size?: string;
|
|
||||||
messages?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default abstract class JDDPage extends StatefulPage<
|
|
||||||
JDDPageState,
|
|
||||||
typeof ComponentPreviewActions
|
|
||||||
> {
|
|
||||||
actions = ComponentPreviewActions;
|
|
||||||
|
|
||||||
previewSizes = ["320", "600", "800", "1024", "1300", "1920"];
|
|
||||||
|
|
||||||
getRegistryComponents() {
|
|
||||||
return registry.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInitialState(ctx: BaseContext) {
|
|
||||||
const all_components = Object.entries(this.getRegistryComponents());
|
|
||||||
const first_component = all_components[0];
|
|
||||||
if (!first_component) {
|
|
||||||
throw new Error("No defined components!");
|
|
||||||
}
|
|
||||||
const [component_name, component] = first_component;
|
|
||||||
const initial_state = {
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
component_name: component_name,
|
|
||||||
args: await component.getExampleValues(makeJDDContext(ctx)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return initial_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapInLayout(
|
|
||||||
ctx: BaseContext,
|
|
||||||
content: Templatable,
|
|
||||||
state: JDDPageState
|
|
||||||
): Templatable {
|
|
||||||
return html(
|
|
||||||
ctx,
|
|
||||||
"Components",
|
|
||||||
content,
|
|
||||||
{
|
|
||||||
morphing: true,
|
|
||||||
preserveScroll: true,
|
|
||||||
autoRefreshCSS: true,
|
|
||||||
navbar: () => ``,
|
|
||||||
bodyClasses: ["jdd-editor"],
|
|
||||||
},
|
|
||||||
(...args) =>
|
|
||||||
tempstream`${defaultHead(...args)}${renderEarlyAssets(
|
|
||||||
registry,
|
|
||||||
documentContainerFromParsed(state.components),
|
|
||||||
makeJDDContext(ctx)
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async preprocessOverrides(
|
|
||||||
_ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
overrides: Record<string, unknown>
|
|
||||||
) {
|
|
||||||
const jdd_context = makeJDDContext(_ctx);
|
|
||||||
if (
|
|
||||||
!hasFieldOfType(
|
|
||||||
"components",
|
|
||||||
overrides,
|
|
||||||
predicates.array(
|
|
||||||
predicates.shape({
|
|
||||||
args: predicates.object,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
for (const [component_index, { component_name }] of Object.entries(
|
|
||||||
state.components
|
|
||||||
)) {
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
if (!component) {
|
|
||||||
throw new Error(`Unknown component: ${component_name}`);
|
|
||||||
}
|
|
||||||
const overrides_for_component = overrides.components[
|
|
||||||
parseInt(component_index)
|
|
||||||
] || { args: {} };
|
|
||||||
const promises = Object.entries(component.getArguments()).map(
|
|
||||||
async ([arg_name, arg]) => {
|
|
||||||
const value = overrides_for_component.args[arg_name];
|
|
||||||
if (value) {
|
|
||||||
const new_value = await arg.receivedToParsed(jdd_context, value);
|
|
||||||
overrides_for_component.args[arg_name] = new_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
return overrides;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
abstract renderParameterButtons(_state: JDDPageState): FlatTemplatable;
|
|
||||||
|
|
||||||
renderComponentArgs<C extends Component>(
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
component: C,
|
|
||||||
args: Record<string, unknown>,
|
|
||||||
index: number
|
|
||||||
) {
|
|
||||||
const jdd_context = makeJDDContext(ctx);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="component-preview-parameters"
|
|
||||||
id={`component-preview-parameters--${index}`}
|
|
||||||
>
|
|
||||||
{Object.entries(component.getArguments()).map(async ([arg_name, arg]) => (
|
|
||||||
<ComponentInput
|
|
||||||
{...{
|
|
||||||
state,
|
|
||||||
arg_path: ["components", index.toString(), "args", arg_name],
|
|
||||||
ctx,
|
|
||||||
arg,
|
|
||||||
value:
|
|
||||||
args[arg_name] === undefined
|
|
||||||
? await arg.getExampleValue(jdd_context)
|
|
||||||
: args[arg_name],
|
|
||||||
onblur: this.rerender(),
|
|
||||||
page: this,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderComponentBlock(
|
|
||||||
ctx: BaseContext,
|
|
||||||
state: JDDPageState,
|
|
||||||
{
|
|
||||||
component_name,
|
|
||||||
args: component_args,
|
|
||||||
}: {
|
|
||||||
component_name: string;
|
|
||||||
args: Record<string, unknown>;
|
|
||||||
},
|
|
||||||
component_index: number
|
|
||||||
) {
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
if (!component) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.renderComponentArgs(
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
component,
|
|
||||||
component_args,
|
|
||||||
component_index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async serializeState(ctx: BaseContext, state: JDDPageState) {
|
|
||||||
const serialized_components = await Promise.all(
|
|
||||||
state.components.map(async ({ component_name, args }) => {
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
const single_result = {
|
|
||||||
component_name,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
args: component
|
|
||||||
? await component.convertParsedToStorage(
|
|
||||||
makeJDDContext(ctx),
|
|
||||||
args
|
|
||||||
)
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
return single_result;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const serialized_state = JSON.stringify({ components: serialized_components });
|
|
||||||
return serialized_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deserializeState(ctx: BaseContext, state_string: string) {
|
|
||||||
const jdd_context = makeJDDContext(ctx);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const raw = JSON.parse(state_string);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
||||||
const components_storage = raw.components;
|
|
||||||
if (!Array.isArray(components_storage)) {
|
|
||||||
throw new Error(
|
|
||||||
"'components' key is not an array, got ${components_storage}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const components_parsed = await Promise.all(
|
|
||||||
components_storage.map(async (entry) => {
|
|
||||||
if (
|
|
||||||
!hasShape(
|
|
||||||
{
|
|
||||||
component_name: predicates.string,
|
|
||||||
args: predicates.object,
|
|
||||||
},
|
|
||||||
entry
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected components[] items to be objects with 'component_name' and 'args' keys, got ${entry}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { component_name, args } = entry;
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
if (!component) {
|
|
||||||
throw new Error("Unknown component: ${component_name}");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
component_name,
|
|
||||||
args: await component.convertStorageToParsed(jdd_context, args),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const result = { components: components_parsed };
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPreParameterButtons(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
_ctx: BaseContext,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
_state: JDDPageState
|
|
||||||
): FlatTemplatable | Promise<FlatTemplatable> {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMessages(_ctx: BaseContext, state: JDDPageState) {
|
|
||||||
return (
|
|
||||||
<ul>
|
|
||||||
{(state.messages || []).map((e) => (
|
|
||||||
<li>{e}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(ctx: BaseContext, state: JDDPageState) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="two-column"
|
|
||||||
id="component-debugger"
|
|
||||||
style="--resizable-column-width: 50vw"
|
|
||||||
data-controller="component-debugger"
|
|
||||||
>
|
|
||||||
<div class="component-arguments" id="component-arguments">
|
|
||||||
{this.renderPreParameterButtons(ctx, state)}
|
|
||||||
{this.renderParameterButtons(state)}
|
|
||||||
{this.renderMessages(ctx, state)}
|
|
||||||
{state.components.map((component, component_index) =>
|
|
||||||
this.renderComponentBlock(ctx, state, component, component_index)
|
|
||||||
)}
|
|
||||||
<code style="max-height: 100px; display: block; overflow: hidden; font-size: 9px; color: #a8a8a8; padding: 1rem;">
|
|
||||||
{this.serializeState(ctx, state)}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="resize-gutter"
|
|
||||||
class="resize-gutter"
|
|
||||||
data-component-debugger-target="gutter"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
id="component-preview"
|
|
||||||
class="component-preview"
|
|
||||||
data-component-debugger-target="preview"
|
|
||||||
>
|
|
||||||
<div class="component-preview__header">
|
|
||||||
<span>Preview</span>
|
|
||||||
<span data-component-debugger-target="component-width"></span>
|
|
||||||
<select
|
|
||||||
name="size"
|
|
||||||
autocomplete="off"
|
|
||||||
class="component-preview-size-select"
|
|
||||||
data-component-debugger-target="size-select"
|
|
||||||
data-action="change->component-debugger#handleWidthDropdown"
|
|
||||||
>
|
|
||||||
{this.previewSizes.map((size) => (
|
|
||||||
<option
|
|
||||||
value={size}
|
|
||||||
selected={size === (state.preview_size || "800")}
|
|
||||||
>
|
|
||||||
{`${size} px`}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<noscript>{this.makeActionButton(state, "change_size")}</noscript>
|
|
||||||
</div>
|
|
||||||
<div class="jdd-container">
|
|
||||||
{render(
|
|
||||||
registry,
|
|
||||||
documentContainerFromParsed(state.components),
|
|
||||||
makeJDDContext(ctx)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import { Controller } from "stimulus";
|
|
||||||
import TurndownService from "turndown";
|
|
||||||
|
|
||||||
export default class PasteToMarkdown extends Controller<HTMLTextAreaElement> {
|
|
||||||
connect() {
|
|
||||||
this.element.addEventListener("paste", (event) => {
|
|
||||||
if (event.clipboardData.types.includes("text/html")) {
|
|
||||||
const turndownService = new TurndownService({
|
|
||||||
headingStyle: "atx",
|
|
||||||
preformattedCode: true,
|
|
||||||
} as any);
|
|
||||||
event.preventDefault();
|
|
||||||
const html = (event.clipboardData.getData("text/html") as string)
|
|
||||||
.replaceAll("\n", " ")
|
|
||||||
// to get rid of some of the style metadata from libreoffice
|
|
||||||
.replace(/^<!doctype.*<body[^>]*>/i, "");
|
|
||||||
document.execCommand("insertText", false, turndownService.turndown(html));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export function printArgPath(path: string[]): string {
|
|
||||||
return path.map((e) => `[${e}]`).join("");
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import { Controller } from "stimulus";
|
|
||||||
import { throttle } from "throttle-debounce";
|
|
||||||
|
|
||||||
export default class SubmitOnInput extends Controller<HTMLTextAreaElement> {
|
|
||||||
sendValues: () => void;
|
|
||||||
connect() {
|
|
||||||
this.sendValues = throttle(
|
|
||||||
500,
|
|
||||||
() => {
|
|
||||||
this.element.closest("form").requestSubmit();
|
|
||||||
},
|
|
||||||
{ noTrailing: false }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
makePermanent() {
|
|
||||||
// this prevents morphing from overwriting the input value with previous half-dane values - https://github.com/hotwired/turbo/issues/1199
|
|
||||||
this.element.setAttribute("data-turbo-permanent", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
makeNotPermanent() {
|
|
||||||
this.element.removeAttribute("data-turbo-permanent");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="533.33331"
|
|
||||||
height="533.33331"
|
|
||||||
viewBox="0 0 15.999999 15.999999"
|
|
||||||
fill="none"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
sodipodi:docname="table-add-column-right.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview2"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.86875"
|
|
||||||
inkscape:cx="265.89928"
|
|
||||||
inkscape:cy="266.47482"
|
|
||||||
inkscape:window-width="1536"
|
|
||||||
inkscape:window-height="928"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg2" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 0,16 H 16 V 0 H 0 Z m 1.5,-1.5 v -2 h 3 v 2 z m 4.5,0 v -13 h 8.5 v 13 z M 4.5,1.5 v 2.51959 h -3 V 1.5 Z m -3,4.01959 h 3 V 7.5 h -3 z M 1.5,9 h 3 v 2 h -3 z"
|
|
||||||
fill="#1f2328"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 9.5,8.5 V 11 H 11 V 8.5 h 2.5001 V 7 H 11 V 4.5 H 9.5 V 7 H 7 v 1.5 z"
|
|
||||||
fill="#1F2328"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#009e00;fill-opacity:1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="533.33331"
|
|
||||||
height="533.33331"
|
|
||||||
viewBox="0 0 15.999999 15.999999"
|
|
||||||
fill="none"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
sodipodi:docname="table-add-row-below.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview2"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.86875"
|
|
||||||
inkscape:cx="265.89928"
|
|
||||||
inkscape:cy="266.47482"
|
|
||||||
inkscape:window-width="1536"
|
|
||||||
inkscape:window-height="928"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg2" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 0,0 H 16 V 16 H 0 Z M 1.5,1.5 V 4 h 3 V 1.5 Z M 6,1.5 V 4 H 9.9999 L 10,1.5 Z m 5.5,0 -10e-5,2.5 H 14.5 V 1.5 Z m 3,4 h -13 v 9 h 13 z"
|
|
||||||
fill="#1f2328"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 7.5,9.5 V 7 H 9 v 2.5 h 2.5001 V 11 H 9 v 2.5 H 7.5 V 11 H 5 V 9.5 Z"
|
|
||||||
fill="#1F2328"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#009e00;fill-opacity:1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,70 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="533.33331"
|
|
||||||
height="533.33331"
|
|
||||||
viewBox="0 0 15.999999 15.999999"
|
|
||||||
fill="none"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
sodipodi:docname="table-add-row-header-below.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview2"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.86875"
|
|
||||||
inkscape:cx="265.89928"
|
|
||||||
inkscape:cy="266.47482"
|
|
||||||
inkscape:window-width="1536"
|
|
||||||
inkscape:window-height="928"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg2" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 0,0 H 16 V 16 H 0 Z M 1.5,1.5 V 4 h 3 V 1.5 Z M 6,1.5 V 4 H 9.9999 L 10,1.5 Z m 5.5,0 -10e-5,2.5 H 14.5 V 1.5 Z m 3,4 h -13 v 9 h 13 z"
|
|
||||||
fill="#1f2328"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 4.4849442,9.891467 V 7.692536 H 5.8043028 V 9.891467 H 8.003322 v 1.319359 H 5.8043028 v 2.198931 H 4.4849442 V 11.210826 H 2.286013 V 9.891467 Z"
|
|
||||||
fill="#1F2328"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573" />
|
|
||||||
<g
|
|
||||||
id="g1"
|
|
||||||
transform="translate(-3.2633543,-4)">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="m 12.442783,13.891467 v -2.198931 h 1.319359 v 2.198931 h 2.199019 v 1.319359 h -2.199019 v 2.198931 h -1.319359 v -2.198931 z"
|
|
||||||
fill="#1F2328"
|
|
||||||
id="path2-6"
|
|
||||||
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573"
|
|
||||||
sodipodi:nodetypes="ccccccccccc" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 16.758927,13.898931 V 11.7 h -1.319359 v 2.198931 h -2.199019 v 1.319359 h 2.199019 v 2.198931 h 1.319359 V 15.21829 Z"
|
|
||||||
fill="#1F2328"
|
|
||||||
id="path2-6-7"
|
|
||||||
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573"
|
|
||||||
sodipodi:nodetypes="ccccccccccc" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.4 KiB |
@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="566.59668"
|
|
||||||
height="533.33331"
|
|
||||||
viewBox="0 0 16.9979 15.999999"
|
|
||||||
fill="none"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
sodipodi:docname="table-delete-column.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview2"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.86875"
|
|
||||||
inkscape:cx="265.89928"
|
|
||||||
inkscape:cy="266.47482"
|
|
||||||
inkscape:window-width="1536"
|
|
||||||
inkscape:window-height="928"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg2" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="m 16,12 v 4 H 0 V 0 H 16 V 4 H 14.5 V 1.5 H 11 v 13 h 3.5 V 12 Z M 1.5,12.5 v 2 h 3 v -2 z m 4.5,0 v 2 h 3.5 v -2 z M 9.5,4.01957 V 1.5 H 6 v 2.51958 z m -5,1e-5 V 1.5 h -3 v 2.51959 z m 0,1.5 -3,10e-6 V 7.5 h 3 z M 4.5,9 h -3 v 2 h 3 z M 6,9 v 2 H 9.5 V 9 Z M 6,7.5 H 9.5 V 5.51957 l -3.5,1e-5 z"
|
|
||||||
fill="#1f2328"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 14.7176,9.0606 15.9373,10.2803 16.9979,9.2197 15.7782,8 16.9979,6.7803 15.9372,5.71967 14.7176,6.9393 13.4979,5.71968 12.4373,6.7803 13.6569,8 l -1.2196,1.2196 1.0606,1.0607 z"
|
|
||||||
fill="#1F2328"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#ff0000;fill-opacity:1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.8 KiB |
@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="533.33331"
|
|
||||||
height="533.33667"
|
|
||||||
viewBox="0 0 15.999999 16.0001"
|
|
||||||
fill="none"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
sodipodi:docname="table-delete-row.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview2"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.86875"
|
|
||||||
inkscape:cx="265.89928"
|
|
||||||
inkscape:cy="266.47482"
|
|
||||||
inkscape:window-width="1536"
|
|
||||||
inkscape:window-height="928"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg2" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 16,0 H 0 V 16 H 4 V 14.5 H 1.5 V 9 h 13 v 5.5 H 12 V 16 h 4 z M 1.5,4 V 1.5 h 3 V 4 Z M 6,4 V 1.5 h 4 L 9.9999,4 Z M 11.4999,4 11.5,1.5 h 3 V 4 Z m 0,1.5 H 14.5 v 2 H 11.4998 Z M 4.5,7.5 h -3 v -2 h 3 z m 1.5,0 v -2 h 3.9999 l -10e-5,2 z"
|
|
||||||
fill="#1f2328"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M 8,14.7804 9.2197,16.0001 10.2803,14.9395 9.0606,13.7197 10.2802,12.5001 9.2196,11.4395 8,12.6591 6.7803,11.4395 5.71968,12.5001 6.9393,13.7197 5.71967,14.9394 6.7803,16.0001 Z"
|
|
||||||
fill="#1F2328"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#ff0000;fill-opacity:1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.8 KiB |
@ -1,48 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
fill="#000000"
|
|
||||||
width="800px"
|
|
||||||
height="800px"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
role="img"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
sodipodi:docname="gui-table-col-after-svgrepo-com.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview2"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.86875"
|
|
||||||
inkscape:cx="399.42446"
|
|
||||||
inkscape:cy="400"
|
|
||||||
inkscape:window-width="1536"
|
|
||||||
inkscape:window-height="928"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg2" />
|
|
||||||
<path
|
|
||||||
d="M.52539062.00195313C.23546962.00195313 0 .17085937 0 .38085938V13.623047c0 .2101.23546962.378906.52539062.378906H5.3164062c.2900214 0 .5234375-.168806.5234376-.378906V.38085938c0-.21-.2334162-.37890626-.5234376-.37890626H.52539062zM1.2089844 1.0605469h3.4199218v3.2148437H1.2089844V1.0605469zm0 4.2832031h3.4199218v3.2148438H1.2089844V5.34375zm0 4.2832031h3.4199218V12.84375H1.2089844V9.6269531z"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill="green"
|
|
||||||
d="M9.969294 11.0336q-.2639283 0-.4656736-.1915l-.3880946-.3882q-.1964467-.1966-.1964467-.4709 0-.2795.1964467-.4658l1.5159882-1.5217H6.988903q-.269027 0-.4370814-.194-.1682543-.1941-.1682543-.4684v-.6625q0-.2743.1682543-.4684.1680544-.1941.4370814-.1941h3.642611L9.1155258 4.4865q-.1964467-.1863-.1964467-.4658 0-.2795.1964467-.4658l.3880946-.3882q.1966467-.1966.4656736-.1966.274126 0 .470872.1966l3.368286 3.3693Q14 6.7172 14 7.0018q0 .2795-.191548.471l-3.368286 3.3693q-.201845.1915-.470872.1915z"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#0086be;fill-opacity:1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.2 KiB |
@ -1,48 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
fill="#000000"
|
|
||||||
width="800"
|
|
||||||
height="800"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
role="img"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
sodipodi:docname="table-move-row-up.svg"
|
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview2"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.86875"
|
|
||||||
inkscape:cx="399.42446"
|
|
||||||
inkscape:cy="400"
|
|
||||||
inkscape:window-width="1536"
|
|
||||||
inkscape:window-height="928"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg2" />
|
|
||||||
<path
|
|
||||||
d="M 0,0.525391 C 0,0.23547 0.16890625,0 0.37890626,0 H 13.621094 C 13.831194,0 14,0.23547 14,0.525391 v 4.7910152 c 0,0.2900214 -0.168806,0.5234375 -0.378906,0.5234376 H 0.37890626 C 0.16890626,5.8398438 0,5.6064276 0,5.3164062 Z M 1.0585937,1.208984 V 4.6289062 H 4.2734374 V 1.208984 Z m 4.2832031,0 V 4.6289062 H 8.5566406 V 1.208984 Z m 4.2832031,0 V 4.6289062 H 12.841797 V 1.208984 Z"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill="green"
|
|
||||||
d="m 11.031647,9.969294 q 0,-0.2639283 -0.1915,-0.4656736 l -0.3882,-0.3880946 q -0.1966,-0.1964467 -0.4709002,-0.1964467 -0.2795,0 -0.4658,0.1964467 l -1.5217,1.5159882 V 6.988903 q 0,-0.269027 -0.194,-0.4370814 -0.1941,-0.1682543 -0.4684,-0.1682543 h -0.6625 q -0.2743,0 -0.4684,0.1682543 -0.1941,0.1680544 -0.1941,0.4370814 v 3.642611 l -1.5216,-1.5159882 q -0.1863,-0.1964467 -0.4658,-0.1964467 -0.2795,0 -0.4658,0.1964467 l -0.3882,0.3880946 q -0.1966,0.1966467 -0.1966,0.4656736 0,0.274126 0.1966,0.470872 l 3.3693,3.368286 Q 6.7152468,14 6.9998468,14 q 0.2795,0 0.471,-0.191548 l 3.3693002,-3.368286 q 0.1915,-0.201845 0.1915,-0.470872 z"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#0086be;fill-opacity:1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
fill="#000000"
|
|
||||||
width="800"
|
|
||||||
height="800"
|
|
||||||
viewBox="0 0 14 14"
|
|
||||||
role="img"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
version="1.1"
|
|
||||||
id="svg2"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<path
|
|
||||||
d="M 14,13.474609 C 14,13.76453 13.831094,14 13.621094,14 H 0.378906 C 0.168806,14 0,13.76453 0,13.474609 V 8.6835938 C 0,8.3935724 0.168806,8.1601563 0.378906,8.1601562 h 13.242188 c 0.21,0 0.378906,0.2334162 0.378906,0.5234376 z M 12.941406,12.791016 V 9.3710938 H 9.7265626 v 3.4199222 z m -4.2832028,0 V 9.3710938 H 5.4433594 v 3.4199222 z m -4.2832031,0 V 9.3710938 H 1.158203 v 3.4199222 z"
|
|
||||||
id="path1" />
|
|
||||||
<path
|
|
||||||
fill="green"
|
|
||||||
d="m 2.968353,4.030706 q 0,0.2639283 0.1915,0.4656736 l 0.3882,0.3880946 q 0.1966,0.1964467 0.4709002,0.1964467 0.2795,0 0.4658,-0.1964467 l 1.5217,-1.5159882 v 3.642611 q 0,0.269027 0.194,0.4370814 0.1941,0.1682543 0.4684,0.1682543 h 0.6625 q 0.2743,0 0.4684,-0.1682543 0.1941,-0.1680544 0.1941,-0.4370814 V 3.368486 l 1.5216,1.5159882 q 0.1863,0.1964467 0.4658,0.1964467 0.2794998,0 0.4657998,-0.1964467 l 0.3882,-0.3880946 q 0.1966,-0.1966467 0.1966,-0.4656736 0,-0.274126 -0.1966,-0.470872 L 7.4659532,0.191548 Q 7.2847532,0 7.0001532,0 q -0.2795,0 -0.471,0.191548 L 3.159853,3.559834 q -0.1915,0.201845 -0.1915,0.470872 z"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#0086be;fill-opacity:1" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,93 +0,0 @@
|
|||||||
.title--components {
|
|
||||||
body {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.two-column {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 15px min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-gutter {
|
|
||||||
background-color: gray;
|
|
||||||
cursor: ew-resize;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: transform 200ms, opacity 200ms;
|
|
||||||
&.restarting {
|
|
||||||
transform: scale(0.99);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-preview {
|
|
||||||
width: var(--resizable-column-width);
|
|
||||||
|
|
||||||
& > fieldset {
|
|
||||||
min-width: 0; /* default is min-content and that causes overflow*/
|
|
||||||
max-height: calc(100vh - 75px);
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.component-arguments {
|
|
||||||
max-height: calc(100vh - 80px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-preview-parameters {
|
|
||||||
fieldset {
|
|
||||||
background-color: #80808024;
|
|
||||||
|
|
||||||
table {
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
outline: 0.5px solid #0000006b;
|
|
||||||
|
|
||||||
&.subdued > * {
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.subdued:hover > * {
|
|
||||||
opacity: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sticky {
|
|
||||||
position: sticky;
|
|
||||||
background-color: #ececec;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sticky--left {
|
|
||||||
left: 0;
|
|
||||||
box-shadow: 5px 0px 10px -4px #00000047;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sticky--top {
|
|
||||||
top: 0;
|
|
||||||
box-shadow: 0px 5px 10px -4px #00000047;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-preview {
|
|
||||||
* {
|
|
||||||
transition: all 150ms;
|
|
||||||
}
|
|
||||||
container-type: inline-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (scripting: none) {
|
|
||||||
body {
|
|
||||||
min-width: max-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-preview-size-select {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
@ -1,91 +1,10 @@
|
|||||||
import { documentContainerFromParsed, renderEarlyAssets } from "@sealcode/jdd";
|
import { JDDDebugger } from "@sealcode/jdd-editor";
|
||||||
import type { BaseContext } from "koa";
|
|
||||||
import type { Templatable } from "tempstream";
|
|
||||||
import { tempstream, TempstreamJSX } from "tempstream";
|
|
||||||
import type { Stringifiable } from "tempstream/@types/stringify.js";
|
|
||||||
import html, { defaultHead } from "../html.js";
|
|
||||||
import { registry } from "../jdd-components/components.js";
|
|
||||||
import { makeJDDContext } from "../jdd-context.js";
|
|
||||||
import { ComponentPreviewActions } from "./component-preview/component-preview-actions.js";
|
|
||||||
import type { JDDPageState } from "./component-preview/jdd-page.js";
|
|
||||||
import JDDPage from "./component-preview/jdd-page.js";
|
|
||||||
|
|
||||||
export const actionName = "Components";
|
import html from "src/back/html.js";
|
||||||
|
import { registry } from "src/back/jdd-components/registry.js";
|
||||||
|
import { makeJDDContext } from "src/back/jdd-context.js";
|
||||||
|
import { defaultHead } from "src/back/defaultHead.js";
|
||||||
|
|
||||||
export default new (class JddcomponentDebuggerPage extends JDDPage {
|
export const actionName = "JDDDebugger";
|
||||||
renderParameterButtons(state: JDDPageState): Stringifiable {
|
|
||||||
const all_components = super.getRegistryComponents();
|
|
||||||
if (!state.components[0]) {
|
|
||||||
console.error("No components are present in state.components");
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input type="submit" value="Preview" />
|
|
||||||
<select
|
|
||||||
name="component"
|
|
||||||
onchange={this.makeActionCallback("change_component")}
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
{Object.entries(all_components).map(([name]) => (
|
|
||||||
<option
|
|
||||||
value={name}
|
|
||||||
selected={
|
|
||||||
state.components[0]
|
|
||||||
? name == state.components[0].component_name
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<noscript>{this.makeActionButton(state, "change_component")}</noscript>
|
|
||||||
{this.makeActionButton(state, "randomize_args", "0")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
actions = ComponentPreviewActions;
|
|
||||||
|
|
||||||
async getInitialState(ctx: BaseContext) {
|
export default new JDDDebugger({ html, registry, makeJDDContext, defaultHead });
|
||||||
const component_constants = Object.entries(registry.getAll())[0];
|
|
||||||
if (!component_constants) {
|
|
||||||
throw new Error("Constant component_name or component is udefined");
|
|
||||||
}
|
|
||||||
const [component_name, component] = component_constants;
|
|
||||||
const initial_state = {
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
component_name: component_name,
|
|
||||||
args: await component.getExampleValues(makeJDDContext(ctx)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return initial_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapInLayout(
|
|
||||||
ctx: BaseContext,
|
|
||||||
content: Templatable,
|
|
||||||
state: JDDPageState
|
|
||||||
): Templatable {
|
|
||||||
return html(
|
|
||||||
ctx,
|
|
||||||
"Components",
|
|
||||||
content,
|
|
||||||
{
|
|
||||||
morphing: true,
|
|
||||||
preserveScroll: true,
|
|
||||||
autoRefreshCSS: true,
|
|
||||||
navbar: () => ``,
|
|
||||||
},
|
|
||||||
(...args) =>
|
|
||||||
tempstream`${defaultHead(...args)}${renderEarlyAssets(
|
|
||||||
registry,
|
|
||||||
documentContainerFromParsed(state.components),
|
|
||||||
makeJDDContext(ctx)
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
containerSizes = ["320", "600", "800", "1024", "1300", "1920"];
|
|
||||||
})();
|
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.has-next .next-button,
|
.horizontal-scroller.has-next .next-button,
|
||||||
&.has-prev .prev-button {
|
.horizontal-scroller.has-prev .prev-button {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -13,9 +13,11 @@ export default new (class HorizontalScrollerDemoPage extends Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async render(ctx: Context) {
|
async render(ctx: Context) {
|
||||||
return html(
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
"HorizontalScrollerDemo",
|
title: "HorizontalScrollerDemo",
|
||||||
|
description: "",
|
||||||
|
body: (
|
||||||
<div>
|
<div>
|
||||||
<h2>Always scrolls + has buttons</h2>
|
<h2>Always scrolls + has buttons</h2>
|
||||||
{horizontalScroller({
|
{horizontalScroller({
|
||||||
@ -62,6 +64,7 @@ export default new (class HorizontalScrollerDemoPage extends Page {
|
|||||||
elements: [1, 2, 3, 4].map((n) => <div class="bignum">{n}</div>),
|
elements: [1, 2, 3, 4].map((n) => <div class="bignum">{n}</div>),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -19,9 +19,11 @@ export default new (class ShowFirstRowDemoPage extends Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async render(ctx: Context) {
|
async render(ctx: Context) {
|
||||||
return html(
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
"ShowFirstRowDemo",
|
title: "ShowFirstRowDemo",
|
||||||
|
description: "",
|
||||||
|
body: (
|
||||||
<div>
|
<div>
|
||||||
<h1>showFirstRow() demo</h1>
|
<h1>showFirstRow() demo</h1>
|
||||||
<h2> 9 elements (always overflows)</h2>
|
<h2> 9 elements (always overflows)</h2>
|
||||||
@ -33,7 +35,9 @@ export default new (class ShowFirstRowDemoPage extends Page {
|
|||||||
|
|
||||||
<h2> 3 elements (overflow depends on screen width)</h2>
|
<h2> 3 elements (overflow depends on screen width)</h2>
|
||||||
{showFirstRow({
|
{showFirstRow({
|
||||||
items: [1, 2, 3].map((e) => <div style={makeBoxStyle()}>{e}</div>),
|
items: [1, 2, 3].map((e) => (
|
||||||
|
<div style={makeBoxStyle()}>{e}</div>
|
||||||
|
)),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<h2> 9 elements (always has two rows when collapsed)</h2>
|
<h2> 9 elements (always has two rows when collapsed)</h2>
|
||||||
@ -45,16 +49,18 @@ export default new (class ShowFirstRowDemoPage extends Page {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
9 elements (4 rows in one column mode, two rows in two column mode,
|
9 elements (4 rows in one column mode, two rows in two column
|
||||||
one row otherwise)
|
mode, one row otherwise)
|
||||||
</h2>
|
</h2>
|
||||||
{showFirstRow({
|
{showFirstRow({
|
||||||
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
|
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
|
||||||
<div style={makeBoxStyle()}>{e}</div>
|
<div style={makeBoxStyle()}>{e}</div>
|
||||||
)),
|
)),
|
||||||
how_many_rows: (columns) => (columns == 1 ? 4 : columns == 2 ? 2 : 1),
|
how_many_rows: (columns) =>
|
||||||
|
columns == 1 ? 4 : columns == 2 ? 2 : 1,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -13,9 +13,11 @@ export default new (class SortableDemoPage extends Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async render(ctx: Context) {
|
async render(ctx: Context) {
|
||||||
return html(
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
"SortableDemo",
|
title: "SortableDemo",
|
||||||
|
description: "",
|
||||||
|
body: (
|
||||||
<div>
|
<div>
|
||||||
<h2>Short list</h2>
|
<h2>Short list</h2>
|
||||||
{sortable({
|
{sortable({
|
||||||
@ -32,6 +34,7 @@ export default new (class SortableDemoPage extends Page {
|
|||||||
.map((_, index) => <div>{index}</div>),
|
.map((_, index) => <div>{index}</div>),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -13,14 +13,18 @@ export default new (class FullscreenMenuDemoPage extends Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async render(ctx: Context) {
|
async render(ctx: Context) {
|
||||||
return html(
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
"FullscreenMenuDemo",
|
title: "FullscreenMenuDemo",
|
||||||
|
description: "",
|
||||||
|
body: (
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
/* HTML */ `<style>
|
/* HTML */ `<style>
|
||||||
.fullscreen-menu__panel--panel_1 {
|
.fullscreen-menu__panel--panel_1 {
|
||||||
transform: translateX(-50vw); /* move that one to the left */
|
transform: translateX(
|
||||||
|
-50vw
|
||||||
|
); /* move that one to the left */
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen-menu label {
|
.fullscreen-menu label {
|
||||||
@ -42,8 +46,8 @@ export default new (class FullscreenMenuDemoPage extends Page {
|
|||||||
panel_1: (
|
panel_1: (
|
||||||
<div>
|
<div>
|
||||||
PANEL 1. Click{" "}
|
PANEL 1. Click{" "}
|
||||||
<label for="demo__activate--panel_2">here</label> to go to
|
<label for="demo__activate--panel_2">here</label> to
|
||||||
panel 2<div>And here's some description</div>
|
go to panel 2<div>And here's some description</div>
|
||||||
Go to{" "}
|
Go to{" "}
|
||||||
<label for="demo__activate--panel_3">
|
<label for="demo__activate--panel_3">
|
||||||
Very tall panel 3
|
Very tall panel 3
|
||||||
@ -54,7 +58,9 @@ export default new (class FullscreenMenuDemoPage extends Page {
|
|||||||
<div>
|
<div>
|
||||||
PANEL 2
|
PANEL 2
|
||||||
<div>
|
<div>
|
||||||
<label for="demo__activate--panel_1">←Go back</label>
|
<label for="demo__activate--panel_1">
|
||||||
|
←Go back
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -62,9 +68,9 @@ export default new (class FullscreenMenuDemoPage extends Page {
|
|||||||
<div style="display: flex; flex-flow: column; row-gap: 20px;">
|
<div style="display: flex; flex-flow: column; row-gap: 20px;">
|
||||||
<label for="demo__activate--panel_1">←Go back</label>
|
<label for="demo__activate--panel_1">←Go back</label>
|
||||||
{[
|
{[
|
||||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
|
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
|
||||||
30,
|
28, 29, 30,
|
||||||
].map((n) => (
|
].map((n) => (
|
||||||
<div>{n}</div>
|
<div>{n}</div>
|
||||||
))}
|
))}
|
||||||
@ -83,6 +89,7 @@ export default new (class FullscreenMenuDemoPage extends Page {
|
|||||||
This page is taaaaall - for testing reasons
|
This page is taaaaall - for testing reasons
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
import { withProdApp } from "../test_utils/with-prod-app.js";
|
|
||||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
|
|
||||||
import { FullscreenMenuDemoURL } from "./urls.js";
|
|
||||||
import { getBrowser } from "../test_utils/browser-creator.js";
|
|
||||||
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
|
||||||
|
|
||||||
describe("FullscreenMenuDemo webhint", () => {
|
|
||||||
it("doesn't crash", async function () {
|
|
||||||
return withProdApp(async ({ base_url, rest_api }) => {
|
|
||||||
await rest_api.get(FullscreenMenuDemoURL);
|
|
||||||
await webhintURL(base_url + FullscreenMenuDemoURL);
|
|
||||||
// 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(FullscreenMenuDemoURL);
|
|
||||||
// await webhintHTML(response);
|
|
||||||
});
|
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("FullscreenMenuDemo", () => {
|
|
||||||
let page: Page;
|
|
||||||
let browser: Browser;
|
|
||||||
let context: BrowserContext;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
browser = await getBrowser();
|
|
||||||
context = await browser.newContext();
|
|
||||||
page = await context.newPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await context.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("works as expected", async function () {
|
|
||||||
return withProdApp(async ({ base_url }) => {
|
|
||||||
await page.goto(base_url + FullscreenMenuDemoURL);
|
|
||||||
});
|
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
|
||||||
});
|
|
@ -1,16 +0,0 @@
|
|||||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
|
|
||||||
import { withProdApp } from "../test_utils/with-prod-app.js";
|
|
||||||
import { HelloURL } from "./urls.js";
|
|
||||||
|
|
||||||
describe("Hello", () => {
|
|
||||||
it("doesn't crash", async function () {
|
|
||||||
return withProdApp(async ({ base_url, rest_api }) => {
|
|
||||||
await rest_api.get(HelloURL);
|
|
||||||
await webhintURL(base_url + HelloURL);
|
|
||||||
// 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(HelloURL);
|
|
||||||
// await webhintHTML(response);
|
|
||||||
});
|
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
|
||||||
});
|
|
@ -1,17 +1,17 @@
|
|||||||
import type { Context } from "koa";
|
import type { Context } from "koa";
|
||||||
import { tempstream } from "tempstream";
|
import { TempstreamJSX } from "tempstream";
|
||||||
import { Page } from "@sealcode/sealgen";
|
import { Page } from "@sealcode/sealgen";
|
||||||
import html from "../html.js";
|
import html from "../html.js";
|
||||||
|
|
||||||
export const actionName = "Hello";
|
export const actionName = "Home";
|
||||||
|
|
||||||
export default new (class HelloPage extends Page {
|
export default new (class HomePage extends Page {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async canAccess(_: Context) {
|
async canAccess(_: Context) {
|
||||||
return { canAccess: true, message: "" };
|
return { canAccess: true, message: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(ctx: Context) {
|
async render(ctx: Context) {
|
||||||
return html(ctx, "Hello", tempstream/* HTML */ `<div></div>`);
|
return html({ ctx: ctx, title: "Home", body: <div>Hello</div>, description: "" });
|
||||||
}
|
}
|
||||||
})();
|
})();
|
@ -1,9 +1,40 @@
|
|||||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
|
|
||||||
import { withProdApp } from "../test_utils/with-prod-app.js";
|
import { withProdApp } from "../test_utils/with-prod-app.js";
|
||||||
|
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
|
||||||
|
import { HomeURL } from "./urls.js";
|
||||||
|
import { getBrowser } from "../test_utils/browser-creator.js";
|
||||||
|
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
|
||||||
describe("homepage", function () {
|
describe("Home webhint", () => {
|
||||||
it("passes webhint tests", () =>
|
it("doesn't crash", async function () {
|
||||||
withProdApp(async ({ base_url }) => {
|
return withProdApp(async ({ base_url, rest_api }) => {
|
||||||
await webhintURL(`${base_url}/`);
|
await rest_api.get(HomeURL);
|
||||||
})).timeout(VERY_LONG_TEST_TIMEOUT);
|
await webhintURL(base_url + HomeURL);
|
||||||
|
// 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(HomeURL);
|
||||||
|
// await webhintHTML(response);
|
||||||
|
});
|
||||||
|
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Home", () => {
|
||||||
|
let page: Page;
|
||||||
|
let browser: Browser;
|
||||||
|
let context: BrowserContext;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works as expected", async function () {
|
||||||
|
return withProdApp(async ({ base_url }) => {
|
||||||
|
await page.goto(base_url + HomeURL);
|
||||||
|
});
|
||||||
|
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,28 @@
|
|||||||
import type Router from "@koa/router";
|
import type Router from "@koa/router";
|
||||||
import { Middlewares } from "sealious";
|
|
||||||
import { imageRouter, RESPONSIVE_IMAGES_URL_PATH } from "../image-router.js";
|
import { imageRouter, RESPONSIVE_IMAGES_URL_PATH } from "../image-router.js";
|
||||||
import { MainView } from "./common/main-view.js";
|
|
||||||
import mountAutoRoutes from "./routes.js";
|
import mountAutoRoutes from "./routes.js";
|
||||||
|
import { customUrlView } from "./middlewares/customUrlView.js";
|
||||||
|
import type TheApp from "../app.js";
|
||||||
|
import { Middlewares } from "sealious";
|
||||||
|
|
||||||
export const mainRouter = (router: Router): void => {
|
export const mainRouter = (app: TheApp, router: Router): void => {
|
||||||
const started_at = Date.now(); // necessary to detect aplication restarts
|
const started_at = Date.now(); // necessary to detect aplication restarts
|
||||||
|
|
||||||
router.get("/", Middlewares.extractContext(), async (ctx) => {
|
router.get("(.*)", async (ctx, next) => {
|
||||||
ctx.body = MainView(ctx);
|
// custom redirects
|
||||||
|
if (ctx.body) return;
|
||||||
|
const {
|
||||||
|
items: [redirect],
|
||||||
|
} = await app.collections.redirects
|
||||||
|
.suList()
|
||||||
|
.filter({ from_url: ctx.url })
|
||||||
|
.fetch();
|
||||||
|
if (redirect) {
|
||||||
|
ctx.status = parseInt(redirect.get("type"));
|
||||||
|
ctx.redirect(redirect.get("to_url"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use(Middlewares.extractContext());
|
router.use(Middlewares.extractContext());
|
||||||
@ -18,5 +32,8 @@ export const mainRouter = (router: Router): void => {
|
|||||||
|
|
||||||
router.use(RESPONSIVE_IMAGES_URL_PATH, imageRouter.getRoutes());
|
router.use(RESPONSIVE_IMAGES_URL_PATH, imageRouter.getRoutes());
|
||||||
|
|
||||||
|
app.HTTPServer.koa.use(Middlewares.extractContext());
|
||||||
|
app.HTTPServer.koa.use(customUrlView(app));
|
||||||
|
|
||||||
mountAutoRoutes(router);
|
mountAutoRoutes(router);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import JDDCreator from "./component-preview/jdd-creator.js";
|
|
||||||
|
|
||||||
export const actionName = "JDDPreview";
|
|
||||||
|
|
||||||
export default new (class JDDCreatePreviewPage extends JDDCreator {
|
|
||||||
// uncomment to create whitelist of allowed components
|
|
||||||
// getAllowedComponents() {
|
|
||||||
// return ["nice-box"];
|
|
||||||
// }
|
|
||||||
})();
|
|
@ -1,41 +0,0 @@
|
|||||||
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
|
||||||
import ADMIN_CREDENTIALS from "../default-admin-credentials.js";
|
|
||||||
import { getBrowser } from "../test_utils/browser-creator.js";
|
|
||||||
import { LONG_TEST_TIMEOUT } from "../test_utils/webhint.js";
|
|
||||||
import { withProdApp } from "../test_utils/with-prod-app.js";
|
|
||||||
import { LogoutURL, SignInURL } from "./urls.js";
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("logout test", () => {
|
|
||||||
it("logout", async function () {
|
|
||||||
await withProdApp(async ({ base_url }) => {
|
|
||||||
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}"]`);
|
|
||||||
});
|
|
||||||
}).timeout(LONG_TEST_TIMEOUT);
|
|
||||||
});
|
|
||||||
});
|
|
81
src/back/routes/middlewares/customUrlView.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { Context, Next } from "koa";
|
||||||
|
import { TempstreamJSX } from "tempstream";
|
||||||
|
import { JDD } from "@sealcode/jdd";
|
||||||
|
import type TheApp from "../../app.js";
|
||||||
|
import { registry } from "../../jdd-components/components.js";
|
||||||
|
import { makeJDDContext } from "../../jdd-context.js";
|
||||||
|
import type { RawJDDocument } from "@sealcode/jdd";
|
||||||
|
import { imageRouter } from "../../image-router.js";
|
||||||
|
import type { FilePointer } from "@sealcode/file-manager";
|
||||||
|
import html from "../../html.js";
|
||||||
|
import { tempstream } from "tempstream";
|
||||||
|
import { defaultHead } from "../../defaultHead.js";
|
||||||
|
import { BASE_URL } from "../../config.js";
|
||||||
|
|
||||||
|
interface ContextState {
|
||||||
|
jddNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||||
|
export const customUrlView =
|
||||||
|
(app: TheApp) => async (ctx: Context & ContextState, next: Next) => {
|
||||||
|
try {
|
||||||
|
if (ctx.body) return;
|
||||||
|
const main_domain = new URL(BASE_URL).hostname;
|
||||||
|
const {
|
||||||
|
items: [page],
|
||||||
|
} = await app.collections.page
|
||||||
|
.list(ctx.$context)
|
||||||
|
.filter({
|
||||||
|
url: ctx.url.split("?")[0],
|
||||||
|
...(ctx.hostname !== main_domain
|
||||||
|
? { domain: ctx.hostname }
|
||||||
|
: { domain: "" }),
|
||||||
|
})
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
const pageContent = Array.isArray(page?.get("content"))
|
||||||
|
? (page?.get("content") as RawJDDocument)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (page && pageContent) {
|
||||||
|
const jdd = await JDD.fromStorage(
|
||||||
|
registry,
|
||||||
|
makeJDDContext(ctx),
|
||||||
|
pageContent
|
||||||
|
);
|
||||||
|
const title = page.get("title");
|
||||||
|
const heading = page.get("heading");
|
||||||
|
const hideNavigation = page.get("hideNavigation") || false;
|
||||||
|
ctx.body = html({
|
||||||
|
ctx,
|
||||||
|
title: title || "Aspazja",
|
||||||
|
description: page.get("description") || "",
|
||||||
|
css_clumps: jdd.getAllCSSClumps(),
|
||||||
|
hideNavigation,
|
||||||
|
htmlOptions: {
|
||||||
|
showFooter: !hideNavigation,
|
||||||
|
},
|
||||||
|
body: (
|
||||||
|
<main class="custom-page jdd-outer-container">
|
||||||
|
{heading?.trim() ? <h1>{heading}</h1> : ""}
|
||||||
|
<div class="jdd-container">{jdd.render()}</div>
|
||||||
|
</main>
|
||||||
|
),
|
||||||
|
makeHead: (...args) =>
|
||||||
|
tempstream`${defaultHead(...args)}${jdd.renderEarlyAssets()}`,
|
||||||
|
metaImage: await imageRouter.singleImage(
|
||||||
|
await (page.get("imageForMetadata") as FilePointer).getPath(),
|
||||||
|
400,
|
||||||
|
"jpeg",
|
||||||
|
false
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("error in custom url router middleware:", err);
|
||||||
|
} finally {
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
};
|
@ -109,14 +109,15 @@ export default new (class SignInForm extends Form<typeof fields, void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renderError(ctx: Context, error: PageErrorMessage): Promise<FlatTemplatable> {
|
async renderError(ctx: Context, error: PageErrorMessage): Promise<FlatTemplatable> {
|
||||||
return html(ctx, "SignIn", `${error.message}`);
|
return html({ ctx, title: "SignIn", description: "", body: `${error.message}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
||||||
return html(
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
"SignIn",
|
title: "SignIn",
|
||||||
tempstream`${await super.render(ctx, data, show_field_errors)}`
|
description: "",
|
||||||
);
|
body: tempstream`${await super.render(ctx, data, show_field_errors)}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|