generated from avior/template-web-astro
update
Signed-off-by: Avior <git@avior.me>
This commit is contained in:
parent
f50ec828fb
commit
89d2debb1e
@ -71,5 +71,8 @@ export default defineConfig({
|
|||||||
experimental: {
|
experimental: {
|
||||||
rewriting: true,
|
rewriting: true,
|
||||||
},
|
},
|
||||||
|
devToolbar: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
1018
package-lock.json
generated
1018
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@
|
|||||||
"install:test": "playwright install --with-deps"
|
"install:test": "playwright install --with-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^7",
|
"@astrojs/node": "^8",
|
||||||
"@astrojs/tailwind": "^5",
|
"@astrojs/tailwind": "^5",
|
||||||
"@dzeio/logger": "^3",
|
"@dzeio/logger": "^3",
|
||||||
"@dzeio/object-util": "^1",
|
"@dzeio/object-util": "^1",
|
||||||
|
@ -26,7 +26,7 @@ if (baseProps.type === 'textarea') {
|
|||||||
<p class="prefix">{Astro.props.prefix}</p>
|
<p class="prefix">{Astro.props.prefix}</p>
|
||||||
)}
|
)}
|
||||||
{Astro.props.type === 'textarea' && (
|
{Astro.props.type === 'textarea' && (
|
||||||
<textarea class="textarea transition-[min-height]" {...baseProps} />
|
<textarea data-component="textarea" class="textarea transition-[min-height]" {...baseProps} />
|
||||||
) || (
|
) || (
|
||||||
<input {...baseProps as any} />
|
<input {...baseProps as any} />
|
||||||
)}
|
)}
|
||||||
@ -49,7 +49,7 @@ if (baseProps.type === 'textarea') {
|
|||||||
@apply select-none font-light text-gray-400
|
@apply select-none font-light text-gray-400
|
||||||
}
|
}
|
||||||
.input, .textarea {
|
.input, .textarea {
|
||||||
@apply px-4 w-full bg-gray-100 rounded-lg border-gray-200 min-h-0 border flex items-center gap-2 py-2
|
@apply px-4 w-full bg-gray-100 rounded-lg border-gray-200 min-h-0 border flex items-center gap-2 py-2 text-black
|
||||||
}
|
}
|
||||||
.input textarea, .input input {
|
.input textarea, .input input {
|
||||||
@apply bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none
|
@apply bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none
|
||||||
@ -76,7 +76,7 @@ if (baseProps.type === 'textarea') {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component.handle<HTMLTextAreaElement>('textarea', (it) => {
|
Component.addComponent<HTMLTextAreaElement>('textarea', (it) => {
|
||||||
updateHeight(it)
|
updateHeight(it)
|
||||||
it.addEventListener('input', () => updateHeight(it))
|
it.addEventListener('input', () => updateHeight(it))
|
||||||
})
|
})
|
||||||
|
@ -7,14 +7,14 @@ export interface Props extends Omit<astroHTML.JSX.InputHTMLAttributes, 'type'> {
|
|||||||
block?: boolean
|
block?: boolean
|
||||||
suffix?: string
|
suffix?: string
|
||||||
prefix?: string
|
prefix?: string
|
||||||
options: Array<string | number | {title: string | number, description?: string | number | null}>
|
options: Array<string | number | {value?: string, title: string | number, description?: string | number | null}>
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix', 'options')
|
const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix', 'options')
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- input wrapper -->
|
<!-- input wrapper -->
|
||||||
<label class:list={['parent', 'select', {'w-full': Astro.props.block}]}>
|
<label data-component="select" data-options={Astro.props.options} class:list={['parent', {'w-full': Astro.props.block}]}>
|
||||||
{Astro.props.label && (
|
{Astro.props.label && (
|
||||||
<div class="label">{Astro.props.label}</div>
|
<div class="label">{Astro.props.label}</div>
|
||||||
)}
|
)}
|
||||||
@ -23,14 +23,15 @@ const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix',
|
|||||||
{Astro.props.prefix && (
|
{Astro.props.prefix && (
|
||||||
<p class="prefix">{Astro.props.prefix}</p>
|
<p class="prefix">{Astro.props.prefix}</p>
|
||||||
)}
|
)}
|
||||||
<input readonly {...baseProps as any} />
|
<input type="hidden" {...baseProps} />
|
||||||
|
<input readonly {...objectOmit(baseProps, 'name') as any} />
|
||||||
<ul class="list hidden">
|
<ul class="list hidden">
|
||||||
{Astro.props.options.map((it) => {
|
{Astro.props.options.map((it) => {
|
||||||
if (typeof it !== 'object') {
|
if (typeof it !== 'object') {
|
||||||
it = {title: it}
|
it = {title: it}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li data-value={it.title}>
|
<li data-value={it.value ?? it.title}>
|
||||||
<p>{it.title}</p>
|
<p>{it.title}</p>
|
||||||
{it.description && (
|
{it.description && (
|
||||||
<p class="desc">{it.description}</p>
|
<p class="desc">{it.description}</p>
|
||||||
@ -60,26 +61,28 @@ const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix',
|
|||||||
.input, .textarea {
|
.input, .textarea {
|
||||||
@apply px-4 w-full bg-gray-100 rounded-lg border-gray-200 min-h-0 border flex items-center gap-2 py-2
|
@apply px-4 w-full bg-gray-100 rounded-lg border-gray-200 min-h-0 border flex items-center gap-2 py-2
|
||||||
}
|
}
|
||||||
|
|
||||||
.input textarea, .input input {
|
.input textarea, .input input {
|
||||||
@apply bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none
|
@apply bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none text-black
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
@apply overflow-y-hidden
|
@apply overflow-y-hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@apply absolute top-full mt-2 z-10 bg-gray-50 rounded-lg border-1 border-gray-300 overflow-hidden
|
@apply absolute top-full mt-2 z-10 bg-gray-50 rounded-lg border border-gray-300 overflow-hidden
|
||||||
}
|
}
|
||||||
input:focus + ul {
|
input:focus + ul, ul:hover {
|
||||||
@apply block
|
@apply block
|
||||||
}
|
}
|
||||||
li {
|
ul :global(li) {
|
||||||
@apply px-4 py-2 flex flex-col gap-1 hover:bg-gray-100 cursor-pointer
|
@apply px-4 py-2 flex flex-col gap-1 hover:bg-gray-100 cursor-pointer
|
||||||
}
|
}
|
||||||
li p {
|
ul :global(li p) {
|
||||||
@apply text-gray-600
|
@apply text-gray-600
|
||||||
}
|
}
|
||||||
li p.desc {
|
ul :global(li p.desc) {
|
||||||
@apply text-sm font-light
|
@apply text-sm font-light
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -87,16 +90,31 @@ const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix',
|
|||||||
<script>
|
<script>
|
||||||
import Component from 'libs/Component'
|
import Component from 'libs/Component'
|
||||||
|
|
||||||
Component.handle<HTMLElement>('.select', (it) => {
|
|
||||||
const input = it.querySelector('input')
|
|
||||||
|
Component.addComponent<HTMLElement>('select', (it) => {
|
||||||
|
const input = it.querySelector<HTMLInputElement>('input[type="hidden"]')
|
||||||
|
const displayInput = it.querySelector<HTMLInputElement>('input[readonly]')
|
||||||
const list = it.querySelector('ul')
|
const list = it.querySelector('ul')
|
||||||
if (!input || !list) {
|
if (!input || !list || !displayInput) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
function setSelectValue(value: string, displayValue: string) {
|
||||||
|
input!.value = value
|
||||||
|
input!.setAttribute('value', value)
|
||||||
|
displayInput!.value = displayValue
|
||||||
|
input!.dispatchEvent(new Event('change'))
|
||||||
|
}
|
||||||
|
const value = input.getAttribute('value') ?? displayInput.getAttribute('value')
|
||||||
|
if (value) {
|
||||||
|
const displayValue = list.querySelector(`li[data-value="${value}"]`)
|
||||||
|
if (displayValue) {
|
||||||
|
setSelectValue(value!, displayValue.querySelector('p')?.innerText ?? '')
|
||||||
|
}
|
||||||
|
}
|
||||||
list.querySelectorAll('li').forEach((listItem) => {
|
list.querySelectorAll('li').forEach((listItem) => {
|
||||||
listItem.addEventListener('pointerdown', () => {
|
listItem.addEventListener('click', () => {
|
||||||
input.value = listItem.dataset.value as string
|
setSelectValue(listItem.dataset.value!, listItem.querySelector('p')?.innerText ?? '')
|
||||||
input.dispatchEvent(new Event('change'))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -8,19 +8,21 @@ delete props.header
|
|||||||
delete props.data
|
delete props.data
|
||||||
---
|
---
|
||||||
<table {...props}>
|
<table {...props}>
|
||||||
<thead>
|
<slot>
|
||||||
<tr data-row="0">
|
<thead>
|
||||||
{Astro.props.header?.map((it, idx) => <th data-cell={idx}>{it}</th>)}
|
<tr data-row="0">
|
||||||
</tr>
|
{Astro.props.header?.map((it, idx) => <th data-cell={idx}>{it}</th>)}
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{Astro.props.data?.map((row, rowIdx) => <tr data-row={rowIdx}>{row.map((it, cellIdx) => <td data-cell={cellIdx}>{it}</td>)}</tr>)}
|
<tbody>
|
||||||
</tbody>
|
{Astro.props.data?.map((row, rowIdx) => <tr data-row={rowIdx}>{row.map((it, cellIdx) => <td data-cell={cellIdx}>{it}</td>)}</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</slot>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table {
|
table {
|
||||||
@apply flex w-full flex-col border-1 rounded-lg overflow-clip
|
@apply flex w-full flex-col border rounded-lg overflow-clip
|
||||||
}
|
}
|
||||||
table :global(th) {
|
table :global(th) {
|
||||||
@apply font-medium
|
@apply font-medium
|
||||||
@ -42,6 +44,6 @@ delete props.data
|
|||||||
@apply border-gray-200
|
@apply border-gray-200
|
||||||
}
|
}
|
||||||
table :global(thead) {
|
table :global(thead) {
|
||||||
@apply bg-gray-100 border-b-1 border-gray-200
|
@apply bg-gray-100 border-b border-gray-200 text-black
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -18,7 +18,14 @@ export interface Props extends HeadProps {
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Hyperion from 'libs/Hyperion'
|
import Component from 'libs/Component'
|
||||||
|
import Hyperion from 'libs/Hyperion'
|
||||||
|
|
||||||
Hyperion.setup()
|
Hyperion.setup()
|
||||||
|
.on('error', ({ error }) => {
|
||||||
|
console.log('Hyperion had an error :(', error)
|
||||||
|
})
|
||||||
|
.on('htmlChange', ({ newElement }) => {
|
||||||
|
Component.load(newElement)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
type Fn<T extends HTMLElement> = (el: Component<T>) => void | Promise<void>
|
type Fn<T extends HTMLElement> = (el: Component<T>) => void | Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component client side initialisation class
|
* Client-side Component management class
|
||||||
|
*
|
||||||
|
* just add a `data-component="name"` to your component to make it work flowlessly
|
||||||
*/
|
*/
|
||||||
export default class Component<T extends HTMLElement> {
|
export default class Component<T extends HTMLElement> {
|
||||||
|
private static components: Record<string, (el: HTMLElement) => void | Promise<void>> = {}
|
||||||
private constructor(
|
private constructor(
|
||||||
public element: T
|
public element: T
|
||||||
) {}
|
) {}
|
||||||
@ -50,4 +53,31 @@ export default class Component<T extends HTMLElement> {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static addComponent<T extends HTMLElement>(name: string, fn: (el: T) => void | Promise<void>) {
|
||||||
|
this.components[name] = fn as any
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static load(base?: HTMLElement) {
|
||||||
|
if (base) {
|
||||||
|
this.loadElement(base)
|
||||||
|
}
|
||||||
|
(base ?? document.body).querySelectorAll<HTMLElement>('[data-component]').forEach((it) => {
|
||||||
|
this.loadElement(it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private static loadElement(it: HTMLElement) {
|
||||||
|
const component = it.dataset.component
|
||||||
|
if (!component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const fn = this.components[component]
|
||||||
|
if (!fn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(it)
|
||||||
|
it.removeAttribute('data-component')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,484 +0,0 @@
|
|||||||
import { mustBeObject, objectLoop, objectSize } from '@dzeio/object-util'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hyperion is a library that allow you to make changes to the HTML content by using a mix of `<template />` and `JSON` Reponse from API endpoints
|
|
||||||
*
|
|
||||||
* ex:
|
|
||||||
* ```html
|
|
||||||
* <button
|
|
||||||
* data-action="/api/v1/checkout" // the endpoint to use
|
|
||||||
* data-template="#pouet" // the template to use
|
|
||||||
* >Checkout</button>
|
|
||||||
* <template id="pouet"><p
|
|
||||||
* data-attribute="test" // the JSON path
|
|
||||||
* ></p></template>
|
|
||||||
* ```
|
|
||||||
* will place inside the <button> tag a <p> tag with the text of `test` from the JSON of the API endpoint
|
|
||||||
*
|
|
||||||
* this library is a mix between systems like React/Svelte (which have a lot of frontend handling) and HTMX (which return HTML entirely from the server)
|
|
||||||
*
|
|
||||||
* _Hyperion is a mix of HyperText(HTML) and Action_
|
|
||||||
*
|
|
||||||
* @author Florian Bouillon <contact@avior.me>
|
|
||||||
* @version 1.0.0
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
export default class Hyperion {
|
|
||||||
private static instance: Hyperion
|
|
||||||
private constructor() {
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* setup the library for usage
|
|
||||||
*/
|
|
||||||
public static setup() {
|
|
||||||
if (!this.instance) {
|
|
||||||
this.instance = new Hyperion()
|
|
||||||
}
|
|
||||||
return this.instance
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows you to manually trigger an element
|
|
||||||
* @param el the element to trigger
|
|
||||||
*/
|
|
||||||
public static trigger(el: HTMLElement) {
|
|
||||||
el.dispatchEvent(new Event('hyperion:trigger'))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* initialise hyperion
|
|
||||||
* @param base the base to query from
|
|
||||||
*/
|
|
||||||
private init(base?: HTMLElement) {
|
|
||||||
(base ?? document).querySelectorAll<HTMLElement>('[data-action]').forEach((it) => {
|
|
||||||
this.initItem(it)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* initialize a single element
|
|
||||||
* @param it the element to initialize
|
|
||||||
*/
|
|
||||||
private initItem(it: HTMLElement) {
|
|
||||||
// get the trigger action
|
|
||||||
let trigger = it.dataset.trigger ?? (it.tagName === 'FORM' ? 'submit' : 'click')
|
|
||||||
|
|
||||||
// the triggering options
|
|
||||||
const options: {
|
|
||||||
once?: boolean
|
|
||||||
load?: boolean
|
|
||||||
after?: number
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
// handle options
|
|
||||||
const splitted = trigger.split(' ')
|
|
||||||
for (const item of splitted) {
|
|
||||||
console.log('ah', splitted, item)
|
|
||||||
// item runs only once
|
|
||||||
if (item === 'once') {
|
|
||||||
options.once = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (item === 'load') {
|
|
||||||
options.load = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (item.includes(':')) {
|
|
||||||
const [key, value] = item.split(':', 2)
|
|
||||||
// trigger is done after {x}ms (if another trigger is not done)
|
|
||||||
if (key === 'after') {
|
|
||||||
options.after = parseInt(value!)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger is not special
|
|
||||||
trigger = item
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout: NodeJS.Timeout | undefined
|
|
||||||
|
|
||||||
// the triggering function
|
|
||||||
let fn = (ev?: Event) => {
|
|
||||||
if (ev) {
|
|
||||||
ev.preventDefault()
|
|
||||||
}
|
|
||||||
if (options.after) { // handle options:after
|
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
timeout = undefined
|
|
||||||
}
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
this.processElement(it)
|
|
||||||
timeout = undefined
|
|
||||||
}, options.after)
|
|
||||||
} else {
|
|
||||||
this.processElement(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// handle event only happening once
|
|
||||||
if (options.once) {
|
|
||||||
it.removeEventListener('hyperion:trigger', fn)
|
|
||||||
it.removeEventListener(trigger, fn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// event can also manually be triggered using `hyperion:trigger`
|
|
||||||
it.addEventListener('hyperion:trigger', fn)
|
|
||||||
it.addEventListener(trigger, fn)
|
|
||||||
|
|
||||||
// on load run instantly
|
|
||||||
if (options.load) {
|
|
||||||
this.processElement(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* process an element that has it's trigger triggered
|
|
||||||
* @param it the element to process
|
|
||||||
*/
|
|
||||||
private async processElement(it: HTMLElement) {
|
|
||||||
console.log(it)
|
|
||||||
const action = it.dataset.action // get the URL to try
|
|
||||||
const method = it.dataset.method ?? 'GET' // get the method to use
|
|
||||||
const subPath = it.dataset.path // get a subPath if desired (dot-separated values)
|
|
||||||
let target = it.dataset.target ?? 'innerHTML' // indicate how the data is set
|
|
||||||
const templateQuery = it.dataset.template // get the template query
|
|
||||||
const runQuery = it.dataset.run // get the template query
|
|
||||||
const multiple = it.hasAttribute('data-multiple') // get if the result should be an array
|
|
||||||
const params: Record<string, any> = {} // the request parameters
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PRE REQUEST HANDLING
|
|
||||||
*/
|
|
||||||
|
|
||||||
// handle mendatory elements that are not necessary set
|
|
||||||
if ((!templateQuery && !runQuery) || !action) {
|
|
||||||
it.dispatchEvent(new CustomEvent('hyperion:attributeError', {
|
|
||||||
detail: {
|
|
||||||
reason: 'data-template/data-run or data-action not found'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
throw new Error(`the data-template (${templateQuery}) and data-run (${runQuery}) or data-action (${action}) attribute is not set :(`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(action, window.location.href)
|
|
||||||
|
|
||||||
// pre-process the target attribute
|
|
||||||
let targetEl: HTMLElement | null = it
|
|
||||||
|
|
||||||
// handle if the target has a querySelector needed
|
|
||||||
if (target.includes(' ')) {
|
|
||||||
// split the target and the query
|
|
||||||
let [newTarget, query] = target.split(' ', 2)
|
|
||||||
|
|
||||||
// set the new target
|
|
||||||
target = newTarget!
|
|
||||||
|
|
||||||
// handle if the query start with `this`
|
|
||||||
let base: HTMLElement | Document = document
|
|
||||||
if (query?.startsWith('this')) {
|
|
||||||
query.replace('this', '')
|
|
||||||
base = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// query the targetElement
|
|
||||||
targetEl = base.querySelector<HTMLElement>(query!)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle if the target was not found
|
|
||||||
if (!targetEl) {
|
|
||||||
it.dispatchEvent(new CustomEvent('hyperion:targetError', {
|
|
||||||
detail: {
|
|
||||||
type: 'target not found',
|
|
||||||
data: target
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
throw new Error(`target not found ${target}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (it.tagName === 'INPUT') {
|
|
||||||
const value = (it as HTMLInputElement).value
|
|
||||||
if (value) {
|
|
||||||
params[(it as HTMLInputElement).name] = (it as HTMLInputElement).value
|
|
||||||
}
|
|
||||||
} else if (it.tagName === 'FORM') {
|
|
||||||
const form = (it as HTMLFormElement)
|
|
||||||
for (const item of form.elements) {
|
|
||||||
const name = (item as HTMLInputElement).name
|
|
||||||
const value = (item as HTMLInputElement).value
|
|
||||||
if (value) {
|
|
||||||
params[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REQUEST HANDLING
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (method === 'GET' && objectSize(params) > 0) {
|
|
||||||
objectLoop(params, (value, key) => {
|
|
||||||
url.searchParams.set(key, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the request
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: method,
|
|
||||||
body: method === 'POST' && objectSize(params) > 0 ? JSON.stringify(params) : null
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST REQUEST HANDLING
|
|
||||||
*/
|
|
||||||
|
|
||||||
// handle if the request does not succeed
|
|
||||||
if (res.status >= 400) {
|
|
||||||
it.dispatchEvent(new CustomEvent('hyperion:statusError', {
|
|
||||||
detail: {
|
|
||||||
statusCode: res.status
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
throw new Error(`request returned a ${res.status} error code :(`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// transform the response into JSON
|
|
||||||
let json = await res.json()
|
|
||||||
|
|
||||||
if (runQuery) {
|
|
||||||
const runEl = document.querySelector<HTMLElement>(runQuery)
|
|
||||||
if (!runEl) {
|
|
||||||
it.dispatchEvent(new CustomEvent('hyperion:runError', {
|
|
||||||
detail: {
|
|
||||||
reason: 'run element not found'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
throw new Error(`the run Element for the query ${runQuery} was not found ! :(`)
|
|
||||||
}
|
|
||||||
|
|
||||||
Hyperion.trigger(runEl)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// query the remote template
|
|
||||||
const template = document.querySelector<HTMLTemplateElement>(templateQuery!)
|
|
||||||
if (!template) {
|
|
||||||
it.dispatchEvent(new CustomEvent('hyperion:templateError', {
|
|
||||||
detail: {
|
|
||||||
reason: 'template not found'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
throw new Error(`the Template for the query ${templateQuery} was not found ! :(`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if subPath was set get to the path described
|
|
||||||
if (subPath) {
|
|
||||||
for (const child of subPath.split('.')) {
|
|
||||||
json = json[child]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the template clone(s)
|
|
||||||
const clones: Array<HTMLElement> = []
|
|
||||||
|
|
||||||
// if the query should have multiple clones (data-multiple)
|
|
||||||
if (multiple) {
|
|
||||||
// the data should be an array :D
|
|
||||||
if (!Array.isArray(json)) {
|
|
||||||
it.dispatchEvent(new CustomEvent('hyperion:dataError', {
|
|
||||||
detail: {
|
|
||||||
type: 'data_not_array',
|
|
||||||
data: json
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
throw new Error(`the data inputted should be an array, instead it is an ${typeof json}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loop through the array
|
|
||||||
for (const item of json) {
|
|
||||||
// fill the template element with every values
|
|
||||||
clones.push(this.fillTemplate(template, item))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// fill the template element with the data
|
|
||||||
clones.push(this.fillTemplate(template, json))
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle how the clones are placed
|
|
||||||
switch (target) {
|
|
||||||
case 'outerHTML': // the clones replace the targetted element
|
|
||||||
targetEl.replaceWith(...clones)
|
|
||||||
break;
|
|
||||||
case 'innerHTML': // the clones replace the childs of the targetted element
|
|
||||||
// remove the current childs
|
|
||||||
while (targetEl.firstChild) {
|
|
||||||
targetEl.removeChild(targetEl.firstChild)
|
|
||||||
}
|
|
||||||
// add each clones
|
|
||||||
clones.forEach((it) => targetEl.appendChild(it))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fill the template from the data given
|
|
||||||
*
|
|
||||||
* @param template the template to fill
|
|
||||||
* @param data the data to filel with
|
|
||||||
* @returns the filled HTMLElement
|
|
||||||
*/
|
|
||||||
private fillTemplate(template: HTMLTemplateElement, data: any): HTMLElement {
|
|
||||||
// clone the template
|
|
||||||
const clone = template.content.cloneNode(true) as HTMLElement
|
|
||||||
|
|
||||||
clone.querySelectorAll<HTMLElement>('[data-loop]').forEach((it) => {
|
|
||||||
const attr = it.dataset.loop!
|
|
||||||
const tpl = it.children.item(0)!
|
|
||||||
const childContent = attr === 'this' ? data : data[attr]
|
|
||||||
console.log(it, childContent)
|
|
||||||
if (!Array.isArray(childContent)) {
|
|
||||||
throw new Error('child MUST be an array')
|
|
||||||
}
|
|
||||||
for (let idx = 0; idx < childContent.length; idx++) {
|
|
||||||
const child = tpl!.cloneNode(true) as HTMLElement
|
|
||||||
console.log(child)
|
|
||||||
let childAttr = child.dataset.attribute
|
|
||||||
if (childAttr === 'this') childAttr = idx.toString()
|
|
||||||
child.dataset.attribute = attr + '.' + childAttr
|
|
||||||
it.appendChild(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.removeChild(tpl)
|
|
||||||
})
|
|
||||||
|
|
||||||
// go through every elements that has a attribute to fill
|
|
||||||
clone.querySelectorAll<HTMLElement>('[data-attribute]').forEach((it) => {
|
|
||||||
// get the raw attribute
|
|
||||||
const attrRaw = it.dataset.attribute!
|
|
||||||
|
|
||||||
// parse into an array
|
|
||||||
let attrs: Array<string> = []
|
|
||||||
let quoteCount = 0
|
|
||||||
let current = ''
|
|
||||||
const splitted = attrRaw.split('')
|
|
||||||
for (let idx = 0; idx < splitted.length; idx++) {
|
|
||||||
const char = splitted[idx];
|
|
||||||
if (char === '\'' && splitted[idx - 1] != '\\') {
|
|
||||||
quoteCount += 1
|
|
||||||
continue
|
|
||||||
} else if (char === ' ' && quoteCount % 2 === 0) {
|
|
||||||
attrs.push(current)
|
|
||||||
current = ''
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (char === '\'' && splitted[idx - 1] === '\\') {
|
|
||||||
current = current.slice(0, current.length - 1)
|
|
||||||
}
|
|
||||||
current += char
|
|
||||||
}
|
|
||||||
if (current) {
|
|
||||||
attrs.push(current)
|
|
||||||
}
|
|
||||||
console.log(attrs)
|
|
||||||
|
|
||||||
// loop through each attributes
|
|
||||||
for (let attr of attrs) {
|
|
||||||
// get the display type of the attribute
|
|
||||||
let target = it.dataset.type ?? 'text'
|
|
||||||
|
|
||||||
// detect if it should be set as an attribute instead
|
|
||||||
const setToAttribute = attr.includes(':')
|
|
||||||
if (setToAttribute) {
|
|
||||||
const splitted = attr.split(':', 2)
|
|
||||||
attr = splitted.pop()!
|
|
||||||
target = splitted[0]!
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle processing of string
|
|
||||||
let content: string
|
|
||||||
// handle a string with attribute processing
|
|
||||||
if (attr.includes('{')) {
|
|
||||||
let res = /\{(.*?)\}/g.exec(attr)
|
|
||||||
while (res) {
|
|
||||||
attr = attr.replace(`${res[0]}`, data[res[1]!])
|
|
||||||
res = /\{(.*?)\}/g.exec(attr)
|
|
||||||
}
|
|
||||||
content = attr
|
|
||||||
} else { // handle basic string
|
|
||||||
content = attr === 'this' ? data : objectGet(data, attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setToAttribute) { // set it as attribute
|
|
||||||
it.setAttribute(target, content)
|
|
||||||
} else {
|
|
||||||
// get the real target function
|
|
||||||
if (target === 'html') {
|
|
||||||
target = 'innerHTML'
|
|
||||||
} else if ( target === 'text') {
|
|
||||||
target = 'innerText'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(target in it)) {
|
|
||||||
template.dispatchEvent(new CustomEvent('hyperion:dataError', {
|
|
||||||
detail: {
|
|
||||||
type: 'data-type invalid',
|
|
||||||
data: target
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
throw new Error(`the data-type is not valid (${target}), it should be one of ('innerHTML' | 'innerText' | 'outerHTML' | 'outerText')`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set it has inner/outer HTML/Text
|
|
||||||
it[target as 'innerHTML' | 'innerText' | 'outerHTML' | 'outerText'] = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// idk if necessary but remove the attributes from the final HTML
|
|
||||||
// it.removeAttribute('data-attribute')
|
|
||||||
// it.removeAttribute('data-type')
|
|
||||||
})
|
|
||||||
|
|
||||||
// setup the clone to work if it contains Hyperion markup
|
|
||||||
this.init(clone)
|
|
||||||
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* go through an object to get a specific value
|
|
||||||
*
|
|
||||||
* note: it will be slower than getting it directly but it allows some dynamism and other libs to query things
|
|
||||||
*
|
|
||||||
* @param obj the object to go through
|
|
||||||
* @param path the path to follow (if path is a string it will be splitted with `.` and ints will be parsed)
|
|
||||||
*
|
|
||||||
* @returns the value or undefined
|
|
||||||
*/
|
|
||||||
function objectGet(obj: object, path: Array<string | number | symbol> | string): any | undefined {
|
|
||||||
mustBeObject(obj)
|
|
||||||
|
|
||||||
// transform path into an Array
|
|
||||||
if (typeof path === 'string') {
|
|
||||||
path = path.split('.').map((it) => /^\d+$/g.test(it) ? parseInt(it) : it)
|
|
||||||
}
|
|
||||||
let pointer: object = obj
|
|
||||||
for (let index = 0; index < path.length; index++) {
|
|
||||||
const key = path[index]
|
|
||||||
const nextIndex = index + 1;
|
|
||||||
if (typeof key === 'undefined' || !Object.prototype.hasOwnProperty.call(pointer, key) && nextIndex < path.length) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
// if last index
|
|
||||||
if (nextIndex === path.length) {
|
|
||||||
return (pointer as any)[key]
|
|
||||||
}
|
|
||||||
// move pointer to new key
|
|
||||||
pointer = (pointer as any)[key]
|
|
||||||
}
|
|
||||||
}
|
|
133
src/libs/Hyperion/README.md
Normal file
133
src/libs/Hyperion/README.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# Hyperion
|
||||||
|
|
||||||
|
Hyperion is a library that allow you to make changes to the HTML content by using a mix of `<template />` and `JSON` Reponse from API endpoints
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i hyperion
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Hyperion from 'hyperion'
|
||||||
|
Hyperion.setup()
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button
|
||||||
|
data-trigger="" <-- How the input will be triggered
|
||||||
|
|
||||||
|
data-input="/api/v1/" <-- get from a remote source (result MUST be in JSON)
|
||||||
|
data-input="delete:/api/v1/" <-- the method is chosen by using a method
|
||||||
|
data-input="run:action" <-- will run with param0 being the HTMLElement & param1 being the input data in JSON (form only)
|
||||||
|
|
||||||
|
-- IO --
|
||||||
|
|
||||||
|
data-path="path" <-- run the output on a child instead of the base object
|
||||||
|
data-multiple <-- (ONLY if response object is an array) will display multiple elements instead of one
|
||||||
|
|
||||||
|
data-output="template location|body|this{location} inner|outer|append" <-- Will fill the template, and display it in the location (inner,outer define if it replace or is a child)
|
||||||
|
data-output="run:action" <-- will run with param0 being the HTMLElement & param1 being the data in JSON
|
||||||
|
data-output="hyp:query" <-- Will run another Hyperion element by the query
|
||||||
|
></button>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Template
|
||||||
|
it MUST only have one child
|
||||||
|
```html
|
||||||
|
<template id="pokemon">
|
||||||
|
<p
|
||||||
|
data-attribute="key" <-- set the inner text of the element
|
||||||
|
data-attribute="html:key" <-- set the inner HTML of the element
|
||||||
|
data-attribute="outertext:key" <-- set the outer text of the element
|
||||||
|
data-attribute="outerhtml:key" <-- set the outer HTML of the element
|
||||||
|
data-attribute="any-other-attribute:key" <-- set the attribute of the element
|
||||||
|
|
||||||
|
data-loop="key" <-- child elements will loop through each items of the array defined by `key`
|
||||||
|
></p>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
|
||||||
|
one of the `data-input` or `data-output` MUST be set so that everything work.
|
||||||
|
|
||||||
|
#### data-trigger: (optionnal)
|
||||||
|
the different trigger available (default: 'submit' for <form> else 'click' for the rest) (multiple can be used)
|
||||||
|
- load: action is run on start
|
||||||
|
- once: only run Hyperion once (manual trigger can still happens if `force` is `true`)
|
||||||
|
- `after:xx`: trigger will defer until xx time of ms passed (allow to dedup requests)
|
||||||
|
- HTMLListenerEvent: any HTMLElement event available
|
||||||
|
|
||||||
|
#### data-input: (optionnal)
|
||||||
|
if data-input is not set it will directly got for data-output with an empty object
|
||||||
|
- url: will query the URL using a GET method for JSON data
|
||||||
|
- `method:url`: will query the URL using the method for JSON data
|
||||||
|
- `run:action`: Get informations by running an action
|
||||||
|
|
||||||
|
#### data-path: (optionnal)
|
||||||
|
a Subpath of the input data to lessen strain on the output
|
||||||
|
|
||||||
|
#### data-multiple: (optionnal)
|
||||||
|
if the input is an array and data-multiple is set, the output will be using array
|
||||||
|
|
||||||
|
#### data-output: (optionnal)
|
||||||
|
- template: The template used to display
|
||||||
|
- location: the location to display the element (default: this)
|
||||||
|
note: by using `this` as a prefix it will query relative to the current element (ex: `this#pouet`)
|
||||||
|
- replace|child: replace or append as child (default: child)
|
||||||
|
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
Actions are elements defined in Hyperion that run code to fetch informations or run custom code
|
||||||
|
|
||||||
|
there is two types of actions:
|
||||||
|
- input Action
|
||||||
|
An input Action MUST return a JSON object and will have access to the HTMLElement that was triggered
|
||||||
|
`(element?: HTMLElement, input?: object) => Promise<object> | object`
|
||||||
|
the element can be omitted when trigger is done remotely
|
||||||
|
the input can be available depending on the source
|
||||||
|
|
||||||
|
- output Action
|
||||||
|
`(element: HTMLElement, output: object) => Promise<void> | void`
|
||||||
|
the output is the data fetched by the input
|
||||||
|
|
||||||
|
example output Action
|
||||||
|
`popup`
|
||||||
|
will search a popup template by using an additionnal attribute `data-template` and fill the elements to display a popup
|
||||||
|
|
||||||
|
builtin output actions
|
||||||
|
- `reload`: Reload the current page
|
||||||
|
|
||||||
|
### Hyperion Class
|
||||||
|
|
||||||
|
- Static
|
||||||
|
- setup()
|
||||||
|
- Methods
|
||||||
|
- on('trigger', ({ target: HTMLElement, trigger: string, force: boolean }) => void)
|
||||||
|
- on('htmlChanged', ({ rootElement: HTMLElement }) => void)
|
||||||
|
- on('error', ({ error: Error }) => void)
|
||||||
|
- trigger('trigger', HTMLElement, options?: { force: boolean })
|
||||||
|
- addInputAction(action)
|
||||||
|
- addOutputAction(action)
|
||||||
|
- fillTemplate(template: HTMLTemplateElement, data: object) => HTMLElement
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### pushing changes of a textarea to the remote server
|
||||||
|
|
||||||
|
```html
|
||||||
|
<textarea data-trigger="keyup after:200" data-input="post:/api/v1/text" name="text">
|
||||||
|
base text
|
||||||
|
</textarea>
|
||||||
|
```
|
||||||
|
|
||||||
|
It will send this as a POST request to the url `/api/v1/text` with the body below
|
||||||
|
```json
|
||||||
|
{"text": "base text"}
|
||||||
|
```
|
662
src/libs/Hyperion/index.ts
Normal file
662
src/libs/Hyperion/index.ts
Normal file
@ -0,0 +1,662 @@
|
|||||||
|
import { isObject, objectGet, objectLoop } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
type MaybePromise<T = void> = T | Promise<T>
|
||||||
|
|
||||||
|
type InputAction = (element?: HTMLElement, input?: object) => MaybePromise<object>
|
||||||
|
type OutputAction = (element: HTMLElement, output: object) => MaybePromise
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
force?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Events {
|
||||||
|
trigger: (ev: {
|
||||||
|
/**
|
||||||
|
* the source element
|
||||||
|
*/
|
||||||
|
target: HTMLElement,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the trigger that triggered
|
||||||
|
*/
|
||||||
|
trigger: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is the trigger forced or not
|
||||||
|
*/
|
||||||
|
force: boolean
|
||||||
|
}) => MaybePromise
|
||||||
|
htmlChange: (ev: { newElement: HTMLElement }) => MaybePromise
|
||||||
|
error: (ev: { error: Error, params?: object | undefined }) => MaybePromise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hyperion is a library that allow you to make changes to the HTML content by using a mix of `<template />` and `JSON` Reponse from API endpoints
|
||||||
|
*
|
||||||
|
* @author Florian Bouillon <contact@avior.me>
|
||||||
|
* @version 2.0.0
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
export default class Hyperion {
|
||||||
|
private static instance: Hyperion
|
||||||
|
private constructor() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private events: Partial<Record<keyof Events, Array<Events[keyof Events]>>> = {}
|
||||||
|
private inputActions: Record<string, InputAction> = {}
|
||||||
|
private outputActions: Record<string, OutputAction> = {
|
||||||
|
reload() {
|
||||||
|
window.location.reload()
|
||||||
|
},
|
||||||
|
debug(el, out) {
|
||||||
|
console.log('Output launched !', el, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setup the library for usage
|
||||||
|
*/
|
||||||
|
public static setup() {
|
||||||
|
if (!Hyperion.instance) {
|
||||||
|
Hyperion.instance = new Hyperion()
|
||||||
|
}
|
||||||
|
return Hyperion.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
public addOutputAction(action: string, ev: OutputAction | null) {
|
||||||
|
if (ev === null) {
|
||||||
|
delete this.outputActions[action]
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
this.outputActions[action] = ev
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add an event to Hyperion
|
||||||
|
*
|
||||||
|
* @param event the event to register on
|
||||||
|
* @param ev the event that will run
|
||||||
|
*/
|
||||||
|
public on<T extends keyof Events>(event: T, ev: Events[T]): this {
|
||||||
|
if (!this.events[event]) {
|
||||||
|
this.events[event] = []
|
||||||
|
}
|
||||||
|
this.events[event]?.push(ev)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* manually trigger an element
|
||||||
|
*
|
||||||
|
* @param element the element to trigger
|
||||||
|
*/
|
||||||
|
public trigger(element: HTMLElement): this {
|
||||||
|
this.processInput(element)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit<T extends keyof Events>(ev: T, params: Parameters<Events[T]>[0]) {
|
||||||
|
const listeners = (this.events[ev] ?? []) as Array<Events[T]>
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener(params as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialise hyperion
|
||||||
|
* @param base the base to query from
|
||||||
|
*/
|
||||||
|
private init(base?: HTMLElement) {
|
||||||
|
// setup on itself when possible
|
||||||
|
if (base && (base.dataset.output || base.dataset.input)) {
|
||||||
|
this.setupTrigger(base)
|
||||||
|
}
|
||||||
|
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||||
|
(base ?? document).querySelectorAll<HTMLElement>('[data-output],[data-input]').forEach((it) => {
|
||||||
|
this.setupTrigger(it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupTrigger(it: HTMLElement) {
|
||||||
|
// get the trigger action
|
||||||
|
const isForm = it.tagName === 'FORM'
|
||||||
|
const triggers: Array<string> = []
|
||||||
|
|
||||||
|
// the triggering options
|
||||||
|
const options: {
|
||||||
|
once?: boolean
|
||||||
|
load?: boolean
|
||||||
|
after?: number
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
// handle options
|
||||||
|
const splitted = this.betterSplit(it.dataset.trigger ?? '')
|
||||||
|
for (const item of splitted) {
|
||||||
|
const { prefix, value } = this.decodeParam(item)
|
||||||
|
switch (prefix ?? value) {
|
||||||
|
case 'once':
|
||||||
|
options.once = true
|
||||||
|
break
|
||||||
|
case 'load':
|
||||||
|
options.load = true
|
||||||
|
break
|
||||||
|
case 'after':
|
||||||
|
options.after = Number.parseInt(value, 10)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
triggers.push(item)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggers.length === 0) {
|
||||||
|
triggers.push(isForm ? 'submit' : 'click')
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
|
// the triggering function
|
||||||
|
const fn = (ev?: Event) => {
|
||||||
|
if (isForm && ev) {
|
||||||
|
ev.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle event only happening once
|
||||||
|
if (options.once) {
|
||||||
|
for (const trigger of triggers) {
|
||||||
|
it.removeEventListener(trigger, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// special case for option `after:xxx`
|
||||||
|
if (options.after) {
|
||||||
|
if (timeout) { // remove existing timer
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// set a new timer
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
// process
|
||||||
|
this.processInput(it)
|
||||||
|
timeout = undefined
|
||||||
|
}, options.after)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// run it
|
||||||
|
this.processInput(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// event can also manually be triggered using `hyperion:trigger`
|
||||||
|
for (const trigger of triggers) {
|
||||||
|
it.addEventListener(trigger, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// on load run instantly
|
||||||
|
if (options.load) {
|
||||||
|
this.processInput(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processInput(it: HTMLElement, data?: object, options?: Options): Promise<void> {
|
||||||
|
// get the input
|
||||||
|
let input = it.dataset.input
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Params
|
||||||
|
*/
|
||||||
|
const params: object = data ?? {}
|
||||||
|
|
||||||
|
|
||||||
|
// parse form values into input params
|
||||||
|
if (it.tagName === 'FORM') {
|
||||||
|
const formData = new FormData(it as HTMLFormElement)
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
params[key] = value
|
||||||
|
})
|
||||||
|
// parse input value into input param
|
||||||
|
} else if (it.tagName === 'INPUT' && (it as HTMLInputElement).name) {
|
||||||
|
params[(it as HTMLInputElement).name] = (it as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.dataset.params) {
|
||||||
|
const exchange = this.betterSplit(it.dataset.params)
|
||||||
|
for (const item of exchange) {
|
||||||
|
const { prefix, value } = this.decodeParam(item)
|
||||||
|
if (!prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
params[prefix] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// if data-input is not set goto output
|
||||||
|
if (!input) {
|
||||||
|
return this.processOutput(it, params, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { prefix, value } = this.decodeParam(input)
|
||||||
|
|
||||||
|
// handle running an input Action
|
||||||
|
if (prefix === 'run') {
|
||||||
|
const inputAction = this.inputActions[value]
|
||||||
|
if (!inputAction) {
|
||||||
|
throw this.makeError(
|
||||||
|
it,
|
||||||
|
`Input Action not found (${value}), you might need to add it using Hyperion.addInputAction()`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await inputAction(it, params)
|
||||||
|
|
||||||
|
return this.processOutput(it, res, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
let method = 'GET'
|
||||||
|
|
||||||
|
if (prefix) {
|
||||||
|
method = prefix.toUpperCase()
|
||||||
|
input = value
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(method, input)
|
||||||
|
|
||||||
|
const url = new URL(input, window.location.href)
|
||||||
|
let body: string | FormData | null = null
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
console.log('going into GET mode')
|
||||||
|
objectLoop(params, (value, key) => {
|
||||||
|
url.searchParams.set(key, value)
|
||||||
|
})
|
||||||
|
} else if (it.getAttribute('enctype') === 'multipart/form-data') {
|
||||||
|
const formData = new FormData()
|
||||||
|
objectLoop(params, (value, key) => {
|
||||||
|
formData.set(key, value)
|
||||||
|
})
|
||||||
|
body = formData
|
||||||
|
} else {
|
||||||
|
body = JSON.stringify(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the request
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
body: body
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle if the request does not succeed
|
||||||
|
if (res.status >= 400) {
|
||||||
|
throw this.makeError(it, `request returned a ${res.status} error code :(`, { statusCode: res.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform the response into JSON
|
||||||
|
return this.processOutput(it, await res.json(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processOutput(it: HTMLElement, dataTmp: object, options?: Options): Promise<void> {
|
||||||
|
let data = dataTmp
|
||||||
|
const output = it.dataset.output
|
||||||
|
if (!output) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const subpath = it.dataset.path
|
||||||
|
if (subpath) {
|
||||||
|
data = objectGet(data, subpath) as object
|
||||||
|
}
|
||||||
|
|
||||||
|
const { prefix, value } = this.decodeParam(output)
|
||||||
|
|
||||||
|
if (prefix === 'run') {
|
||||||
|
const outputActions = this.outputActions[value]
|
||||||
|
if (!outputActions) {
|
||||||
|
throw this.makeError(it, `Output Action Action not found (${value}), you might need to add it using Hyperion.addOutputAction()`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputActions(it, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix === 'hyp') {
|
||||||
|
const hypItem = document.querySelector<HTMLElement>(value)
|
||||||
|
|
||||||
|
if (!hypItem) {
|
||||||
|
throw this.makeError(it, `Could not find Hyperion element using the query (${value})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.processInput(hypItem, data, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
let [
|
||||||
|
templateQuery,
|
||||||
|
locationQuery = null,
|
||||||
|
placement = 'inner'
|
||||||
|
] = this.betterSplit(output)
|
||||||
|
|
||||||
|
const template = document.querySelector<HTMLTemplateElement>(templateQuery as string)
|
||||||
|
|
||||||
|
if (!template || template.tagName !== 'TEMPLATE') {
|
||||||
|
const msg = !template ? `Template not found using query(${templateQuery})` : `Template is not a template ! (tag is ${template.tagName} while it should be TEMPLATE)`
|
||||||
|
throw this.makeError(it, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const placements = ['inner', 'outer', 'append']
|
||||||
|
|
||||||
|
if (locationQuery && placements.includes(locationQuery)) {
|
||||||
|
placement = locationQuery
|
||||||
|
locationQuery = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isArray = 'multiple' in it.dataset
|
||||||
|
const clones = isArray ? (data as Array<object>).map((it) => this.fillTemplate(template, it)) : [this.fillTemplate(template, data)]
|
||||||
|
|
||||||
|
let location = it
|
||||||
|
if (locationQuery) {
|
||||||
|
if (locationQuery === 'body') {
|
||||||
|
location = document.body
|
||||||
|
} else {
|
||||||
|
let origin = document.body
|
||||||
|
if (locationQuery.startsWith('this')) {
|
||||||
|
origin = it
|
||||||
|
locationQuery = locationQuery.slice(4)
|
||||||
|
}
|
||||||
|
const tmp = origin.querySelector<HTMLElement>(locationQuery)
|
||||||
|
if (!tmp) {
|
||||||
|
throw this.makeError(it, `New location not found (origin: ${origin.tagName}, query: ${locationQuery})`)
|
||||||
|
}
|
||||||
|
location = tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
throw this.makeError(it, 'location not found :(')
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (placement) {
|
||||||
|
case 'outer': // the clones replace the targetted element
|
||||||
|
location.replaceWith(...clones)
|
||||||
|
// TODO: might be buggy due to shit
|
||||||
|
for (const clone of clones) {
|
||||||
|
this.emit('htmlChange', { newElement: clone })
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'inner': // the clones replace the childs of the targetted element
|
||||||
|
// remove the current childs
|
||||||
|
while (location.firstChild) {
|
||||||
|
location.removeChild(location.firstChild)
|
||||||
|
}
|
||||||
|
// add each clones
|
||||||
|
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||||
|
clones.forEach((it) => {
|
||||||
|
const appendedChild = location.appendChild(it)
|
||||||
|
this.emit('htmlChange', { newElement: appendedChild })
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'append': {
|
||||||
|
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||||
|
clones.forEach((it) => {
|
||||||
|
document.body.appendChild(it)
|
||||||
|
this.emit('htmlChange', { newElement: it })
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fill the template from the data given
|
||||||
|
*
|
||||||
|
* @param template the template to fill
|
||||||
|
* @param data the data to filel with
|
||||||
|
* @returns the filled HTMLElement
|
||||||
|
*/
|
||||||
|
public fillTemplate(template: HTMLTemplateElement, data: object): HTMLElement {
|
||||||
|
// clone the template
|
||||||
|
let node = template.content.cloneNode(true) as DocumentFragment | HTMLElement
|
||||||
|
|
||||||
|
if (node.childNodes.length > 1) {
|
||||||
|
const parent = document.createElement('div')
|
||||||
|
parent.appendChild(node)
|
||||||
|
node = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = node.firstElementChild as HTMLElement
|
||||||
|
|
||||||
|
return this.fill(clone, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param el the element to fill
|
||||||
|
* @param data the data to fill it with
|
||||||
|
* @returns the filled element (original changed)
|
||||||
|
*/
|
||||||
|
public fill(el: HTMLElement, data: object) {
|
||||||
|
if (el.dataset.loop) {
|
||||||
|
this.fillLoop(el, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
let childLoop = el.querySelector<HTMLElement>('[data-loop]')
|
||||||
|
while (childLoop) {
|
||||||
|
this.fillLoop(childLoop, data)
|
||||||
|
childLoop = el.querySelector<HTMLElement>('[data-loop]')
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through every elements that has a attribute to fill
|
||||||
|
this.fillAttrs(el, data)
|
||||||
|
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||||
|
el.querySelectorAll<HTMLElement>('[data-attribute],[data-input],[data-output],[data-params]').forEach((it) => this.fillAttrs(it, data))
|
||||||
|
|
||||||
|
// setup the clone to work if it contains Hyperion markup
|
||||||
|
this.init(el)
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillLoop(it: HTMLElement, data: object, context: Array<string | number> = []) {
|
||||||
|
const path = it.dataset.loop
|
||||||
|
if (!path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const subElement = objectGet(data, path) as Array<any>
|
||||||
|
for (let idx = 0; idx < subElement.length; idx++) {
|
||||||
|
const currentContext = [...context, path, idx]
|
||||||
|
const child = it.cloneNode(true) as HTMLElement
|
||||||
|
child.removeAttribute('data-loop')
|
||||||
|
|
||||||
|
let childLoop = child.querySelector<HTMLElement>('[data-loop]')
|
||||||
|
while (childLoop) {
|
||||||
|
this.fillLoop(childLoop, data, currentContext)
|
||||||
|
childLoop = child.querySelector<HTMLElement>('[data-loop]')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fillLoop(child, data, currentContext)
|
||||||
|
|
||||||
|
// go through every elements that has a attribute to fill
|
||||||
|
this.fillAttrs(child, data, currentContext)
|
||||||
|
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||||
|
const items = Array.from(child.querySelectorAll<HTMLElement>('[data-attribute],[data-input],[data-output]'))
|
||||||
|
for (const item of items) {
|
||||||
|
this.fillAttrs(item, data, currentContext)
|
||||||
|
}
|
||||||
|
// child.querySelectorAll<HTMLElement>('[data-attribute],[data-input],[data-output]').forEach((it) => this.fillAttrs(it, sub))
|
||||||
|
|
||||||
|
it!.after(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fill the attributes to the element
|
||||||
|
* @param it the element to fill
|
||||||
|
* @param data the data link to this element
|
||||||
|
* @returns the filled element
|
||||||
|
*/
|
||||||
|
private fillAttrs(it: HTMLElement, data: object, context: Array<string | number> = []) {
|
||||||
|
// get the raw attribute
|
||||||
|
const attrRaw = it.dataset.attribute
|
||||||
|
|
||||||
|
for (const attr of ['output', 'input', 'trigger', 'params']) {
|
||||||
|
let value = it.dataset[attr]
|
||||||
|
if (!value || !value.includes('{')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value = this.parseValue(value, data, context)
|
||||||
|
|
||||||
|
it.setAttribute(`data-${attr}`, value)
|
||||||
|
// it.dataset[attr] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof attrRaw !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse into an array
|
||||||
|
const attrs: Array<string> = this.betterSplit(attrRaw)
|
||||||
|
|
||||||
|
// loop through each attributes
|
||||||
|
for (const attrRaw of attrs) {
|
||||||
|
const { prefix = 'innerhtml', value } = this.decodeParam(attrRaw)
|
||||||
|
|
||||||
|
// handle processing of string
|
||||||
|
// handle a string with attribute processing
|
||||||
|
const attr = this.parseValue(value, data, context)
|
||||||
|
|
||||||
|
switch (prefix) {
|
||||||
|
case 'html':
|
||||||
|
case 'innerhtml': {
|
||||||
|
it.innerHTML = attr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'text':
|
||||||
|
case 'innertext': {
|
||||||
|
it.innerText = attr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'outerhtml': {
|
||||||
|
it.outerHTML = attr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'outertext': {
|
||||||
|
it.outerText = attr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
it.setAttribute(prefix, attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// idk if necessary but remove the attributes from the final HTML
|
||||||
|
it.removeAttribute('data-attribute')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A space centered split but it does not split if inside singlequotes
|
||||||
|
*/
|
||||||
|
private betterSplit(input: string): Array<string> {
|
||||||
|
const attrs: Array<string> = []
|
||||||
|
let quoteCount = 0
|
||||||
|
let current = ''
|
||||||
|
const splitted = input.split('')
|
||||||
|
for (let idx = 0; idx < splitted.length; idx++) {
|
||||||
|
const char = splitted[idx];
|
||||||
|
if (char === '\'' && splitted[idx - 1] !== '\\') {
|
||||||
|
quoteCount += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (char === ' ' && quoteCount % 2 === 0) {
|
||||||
|
attrs.push(current)
|
||||||
|
current = ''
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (char === '\'' && splitted[idx - 1] === '\\') {
|
||||||
|
current = current.slice(0, current.length - 1)
|
||||||
|
}
|
||||||
|
current += char
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
attrs.push(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
private decodeParam(str: string): { prefix?: string, value: string } {
|
||||||
|
const index = str.indexOf(':')
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return { value: str }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { prefix: str.slice(0, index), value: str.slice(index + 1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseValue(str: string, data: object, context: Array<string | number>): string {
|
||||||
|
let value = str
|
||||||
|
if (!value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
function findValue(key: string) {
|
||||||
|
if (key.startsWith('this')) {
|
||||||
|
const fromContext = objectGet(data, context)
|
||||||
|
let sliced = key.slice(4)
|
||||||
|
if (sliced.startsWith('.')) {
|
||||||
|
sliced = sliced.slice(1)
|
||||||
|
const tmp = objectGet(fromContext, sliced)
|
||||||
|
if (!tmp) {
|
||||||
|
console.log(3)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return tmp
|
||||||
|
} else {
|
||||||
|
console.log(fromContext, data, context)
|
||||||
|
return fromContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isObject(data) && (Array.isArray(key) && key.length > 0 || key)) {
|
||||||
|
console.log(str, data, context, key)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
const res = objectGet(data, key)
|
||||||
|
if (!res) {
|
||||||
|
console.log(2)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if (!value.includes('{')) {
|
||||||
|
return findValue(value)
|
||||||
|
} else {
|
||||||
|
let res = /\{(.*?)\}/g.exec(value)
|
||||||
|
while (res && res.length >= 2) {
|
||||||
|
const key = res[1]!
|
||||||
|
value = value.replace(`${res[0]}`, findValue(key))
|
||||||
|
|
||||||
|
res = /\{(.*?)\}/g.exec(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeError(el: HTMLElement, message: string, params?: object) {
|
||||||
|
el.dispatchEvent(new CustomEvent('hyperion:error', {
|
||||||
|
detail: {
|
||||||
|
error: message,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
const error = new Error(message)
|
||||||
|
this.emit('error', { error, params })
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import { filter } from './AdapterUtils'
|
|||||||
|
|
||||||
export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
||||||
|
|
||||||
private id!: Array<string>
|
private id: Array<string> = []
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
/**
|
/**
|
||||||
@ -24,15 +24,7 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
* the table name
|
* the table name
|
||||||
*/
|
*/
|
||||||
public readonly table: string,
|
public readonly table: string,
|
||||||
/**
|
|
||||||
* the id(s)
|
|
||||||
*/
|
|
||||||
id?: keyof T | Array<keyof T>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* other secondary keys necessary to update data
|
|
||||||
*/
|
|
||||||
private readonly partitionKeys?: Array<keyof T>,
|
|
||||||
/**
|
/**
|
||||||
* additionnal options to make the adapter work
|
* additionnal options to make the adapter work
|
||||||
*/
|
*/
|
||||||
@ -43,20 +35,14 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
debug?: boolean
|
debug?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (!id) {
|
objectLoop(this.schema.model, (schema, key) => {
|
||||||
objectLoop(schema.model, (value, key) => {
|
if (!isSchemaItem(schema)) {
|
||||||
if (!isSchemaItem(value)) {
|
return
|
||||||
return true
|
}
|
||||||
}
|
if (schema.database?.auto) {
|
||||||
if (!value.database?.unique) {
|
this.id.push(key)
|
||||||
return true
|
}
|
||||||
}
|
})
|
||||||
id = key
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.id = typeof id === 'string' ? [id] : id as Array<string>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make it clearer what it does
|
// TODO: make it clearer what it does
|
||||||
@ -107,65 +93,6 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
public async read(query?: Query<Implementation<T>> | undefined): Promise<DBPull<T>> {
|
public async read(query?: Query<Implementation<T>> | undefined): Promise<DBPull<T>> {
|
||||||
let req: Array<string> = ['SELECT', '*', 'FROM', this.table]
|
let req: Array<string> = ['SELECT', '*', 'FROM', this.table]
|
||||||
|
|
||||||
// list of the differents items in the WHERE statement
|
|
||||||
const whereItems: Array<string> = []
|
|
||||||
// if ((query?.where?.length ?? 0) > 0 && (query?.where?.length !== 1 || query?.where?.[0]?.[1] !== 'includes')) {
|
|
||||||
// for (const it of query?.where ?? []) {
|
|
||||||
// // eslint-disable-next-line max-depth
|
|
||||||
// switch (it[1]) {
|
|
||||||
// case 'in':
|
|
||||||
// // eslint-disable-next-line no-case-declarations
|
|
||||||
// const arr = it[2] as Array<any>
|
|
||||||
|
|
||||||
// whereItems.push(`${String(it[0])} IN (${arr.map(() => '?').join(',')})`)
|
|
||||||
// params.push(...arr)
|
|
||||||
// break
|
|
||||||
|
|
||||||
// case 'equal':
|
|
||||||
// whereItems.push(`${String(it[0])} = ?`)
|
|
||||||
// params.push(it[2])
|
|
||||||
// break
|
|
||||||
|
|
||||||
// case 'after':
|
|
||||||
// whereItems.push(`${String(it[0])} >= ?`)
|
|
||||||
// params.push(it[2])
|
|
||||||
// break
|
|
||||||
|
|
||||||
// case 'before':
|
|
||||||
// whereItems.push(`${String(it[0])} <= ?`)
|
|
||||||
// params.push(it[2])
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (whereItems.length > 0) {
|
|
||||||
req.push('WHERE')
|
|
||||||
for (let idx = 0; idx < whereItems.length; idx++) {
|
|
||||||
const item = whereItems[idx] as string
|
|
||||||
if (idx > 0) {
|
|
||||||
req.push('AND')
|
|
||||||
}
|
|
||||||
req.push(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ORDER BY (not working as we want :()
|
|
||||||
// const sort = query?.$sort
|
|
||||||
// if (sort && sort.length >= 1) {
|
|
||||||
// const suffix = sort[0]?.[1] === 'asc' ? 'ASC' : 'DESC'
|
|
||||||
// req = req.concat(['ORDER', 'BY', sort[0]?.[0] as string, suffix])
|
|
||||||
// }
|
|
||||||
|
|
||||||
// LIMIT (not working because of ORDER BY)
|
|
||||||
// const page: number = query?.page ?? 0
|
|
||||||
// const pageLimit: number | null = query?.limit ?? null
|
|
||||||
// let limit: number | null = null
|
|
||||||
// if (pageLimit && pageLimit > 0) {
|
|
||||||
// limit = pageLimit * (page + 1)
|
|
||||||
// req = req.concat(['LIMIT', limit.toString()])
|
|
||||||
// }
|
|
||||||
|
|
||||||
const client = (await Client.get())!
|
const client = (await Client.get())!
|
||||||
|
|
||||||
if (this.options?.debug) {
|
if (this.options?.debug) {
|
||||||
@ -190,7 +117,7 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataset = res.rows
|
const raw = res.rows
|
||||||
.map((obj) => objectRemap(this.schema.model, (_, key) => ({
|
.map((obj) => objectRemap(this.schema.model, (_, key) => ({
|
||||||
key,
|
key,
|
||||||
value: this.dbToValue(key, (obj as any)[key])
|
value: this.dbToValue(key, (obj as any)[key])
|
||||||
@ -207,45 +134,6 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
.map((it) => this.schema.parse(it))
|
.map((it) => this.schema.parse(it))
|
||||||
.filter((it): it is Implementation<T> => !!it)
|
.filter((it): it is Implementation<T> => !!it)
|
||||||
|
|
||||||
/**
|
|
||||||
* POST QUERY TREATMENT
|
|
||||||
*/
|
|
||||||
// if ((query?.where?.length ?? 0) > 0) {
|
|
||||||
// for (const it of query?.where ?? []) {
|
|
||||||
// // eslint-disable-next-line max-depth
|
|
||||||
// switch (it[1]) {
|
|
||||||
// case 'includes':
|
|
||||||
// dataset = dataset.filter((entry) => entry[it[0]]?.toString()?.includes(it[2]))
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// sort
|
|
||||||
// const sort = query?.$sort
|
|
||||||
// if (sort) {
|
|
||||||
// const sortKey = sort ? sort[0]![0] : objectFind(this.schema.model, (value) => {
|
|
||||||
// if (!isSchemaItem(value)) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// return !!value.database?.created
|
|
||||||
// })
|
|
||||||
// const sortValue = sort ? sort[0]![1] : 'asc'
|
|
||||||
// if (sortKey && sortValue) {
|
|
||||||
// if (sortValue === 'asc') {
|
|
||||||
// dataset = dataset.sort((a, b) => b[sortKey as string]! > a[sortKey as string]! ? 1 : -1)
|
|
||||||
// } else {
|
|
||||||
// dataset = dataset.sort((a, b) => b[sortKey as string]! < a[sortKey as string]! ? 1 : -1)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log(res.rows, req)
|
|
||||||
// post request processing
|
|
||||||
// if (limit) {
|
|
||||||
// dataset = dataset.slice(page * (query?.limit ?? 0), limit)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// temp modification of comportement to use the new and better query system
|
// temp modification of comportement to use the new and better query system
|
||||||
if ((!query || !query?.$sort) && objectFind(this.schema.model, (_, key) => key === 'created')) {
|
if ((!query || !query?.$sort) && objectFind(this.schema.model, (_, key) => key === 'created')) {
|
||||||
// temp fix for the sorting algorithm
|
// temp fix for the sorting algorithm
|
||||||
@ -256,13 +144,13 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
query.$sort = { created: Sort.DESC }
|
query.$sort = { created: Sort.DESC }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let dataset = raw
|
||||||
if (query) {
|
if (query) {
|
||||||
dataset = filter(query, dataset, this.options)
|
dataset = filter(query, dataset, this.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(res)
|
// console.log(res)
|
||||||
return {
|
return {
|
||||||
rows: res.rowCount ?? 0,
|
rows: dataset.length ?? 0,
|
||||||
rowsTotal: res.rowCount ?? 0,
|
rowsTotal: res.rowCount ?? 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageTotal: 1,
|
pageTotal: 1,
|
||||||
@ -307,7 +195,7 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// map the items to update
|
// map the items to update
|
||||||
const keys = objectMap(obj as {}, (_, key) => `${key}=?`)
|
const keys = objectMap(obj as {}, (_, key, idx) => `${key}=$${idx+1}`)
|
||||||
parts.push(keys.join(', '))
|
parts.push(keys.join(', '))
|
||||||
params.push(...objectValues(obj as {}))
|
params.push(...objectValues(obj as {}))
|
||||||
|
|
||||||
@ -320,7 +208,7 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
if (idx > 0) {
|
if (idx > 0) {
|
||||||
parts.push('AND')
|
parts.push('AND')
|
||||||
}
|
}
|
||||||
parts.push(`${key}=?`)
|
parts.push(`${key}=$${params.length+1}`)
|
||||||
const value = obj[key] ?? (typeof id === 'string' ? id : id[key])
|
const value = obj[key] ?? (typeof id === 'string' ? id : id[key])
|
||||||
read[key] = this.valueToDB(key, value)
|
read[key] = this.valueToDB(key, value)
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -329,14 +217,6 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
params.push(value)
|
params.push(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.partitionKeys && this.partitionKeys?.length > 0) {
|
|
||||||
const { data } = await this.read(read)
|
|
||||||
const item = data[0]
|
|
||||||
for (const key of this.partitionKeys) {
|
|
||||||
parts.push('AND', `${key as string}=${this.valueToDB(key, item![key])}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = parts.join(' ')
|
const req = parts.join(' ')
|
||||||
const client = await Client.get()
|
const client = await Client.get()
|
||||||
|
|
||||||
@ -345,7 +225,7 @@ export default class PostgresAdapter<T extends Model> implements DaoAdapter<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await client!.query(`${req}`)
|
const res = await client!.query(req, params)
|
||||||
// console.log(res, req)
|
// console.log(res, req)
|
||||||
if (this.options?.debug) {
|
if (this.options?.debug) {
|
||||||
console.log('post patch result', res, req)
|
console.log('post patch result', res, req)
|
||||||
|
@ -2,11 +2,28 @@ import { type Query } from './Query'
|
|||||||
import type { Implementation, Model } from './Schema'
|
import type { Implementation, Model } from './Schema'
|
||||||
|
|
||||||
export interface DBPull<T extends Model> {
|
export interface DBPull<T extends Model> {
|
||||||
|
/**
|
||||||
|
* total number of rows that are valid with the specified query
|
||||||
|
*/
|
||||||
rows: number
|
rows: number
|
||||||
|
/**
|
||||||
|
* total number of rows in the table
|
||||||
|
*/
|
||||||
rowsTotal: number
|
rowsTotal: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* current page number
|
||||||
|
*/
|
||||||
page: number
|
page: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* total amount of pages
|
||||||
|
*/
|
||||||
pageTotal: number
|
pageTotal: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the data fetched
|
||||||
|
*/
|
||||||
data: Array<Implementation<T>>
|
data: Array<Implementation<T>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,15 +7,16 @@ export default {
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
displayID TEXT,
|
displayid TEXT,
|
||||||
visibility BOOL,
|
visibility BOOL,
|
||||||
archived BOOL
|
archived BOOL
|
||||||
);`)
|
);`)
|
||||||
|
|
||||||
await client.query(`CREATE TABLE IF NOT EXISTS state (
|
await client.query(`CREATE TABLE IF NOT EXISTS state (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
project TEXT,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
color CHAR(6),
|
color CHAR(7),
|
||||||
preset BOOL
|
preset BOOL
|
||||||
)`)
|
)`)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ const schema = new Schema({
|
|||||||
|
|
||||||
description: { type: String, nullable: true },
|
description: { type: String, nullable: true },
|
||||||
|
|
||||||
displayID: { type: String, nullable: true },
|
displayid: { type: String, nullable: true },
|
||||||
visibility: { type: String, nullable: true },
|
visibility: { type: String, nullable: true },
|
||||||
archived: { type: Boolean, defaultValue: false }
|
archived: { type: Boolean, defaultValue: false }
|
||||||
})
|
})
|
||||||
|
@ -210,7 +210,7 @@ export default class Schema<M extends Model> {
|
|||||||
* @returns an object containin ONLY the elements defined by the schema
|
* @returns an object containin ONLY the elements defined by the schema
|
||||||
*/
|
*/
|
||||||
public parse(input: any): Implementation<M> | null {
|
public parse(input: any): Implementation<M> | null {
|
||||||
console.log(input)
|
// console.log(input)
|
||||||
if (!isObject(input)) {
|
if (!isObject(input)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -19,4 +19,4 @@ const schema = new Schema({
|
|||||||
|
|
||||||
export default schema
|
export default schema
|
||||||
|
|
||||||
export type ProjectObj = Impl<typeof schema>
|
export type StateObj = Impl<typeof schema>
|
||||||
|
15
src/pages/api/v1/projects/[id].ts
Normal file
15
src/pages/api/v1/projects/[id].ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import ResponseBuilder from 'libs/ResponseBuilder'
|
||||||
|
import DaoFactory from 'models/DaoFactory'
|
||||||
|
|
||||||
|
export const POST: APIRoute = async (ctx) => {
|
||||||
|
const id = ctx.params.id!
|
||||||
|
const dao = DaoFactory.get('project')
|
||||||
|
const body = objectOmit(await ctx.request.json(), 'label')
|
||||||
|
const res = await dao.patch(id, body)
|
||||||
|
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.body(res)
|
||||||
|
.build()
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
import type { APIRoute } from 'astro'
|
import type { APIRoute } from 'astro'
|
||||||
import ResponseBuilder from 'libs/ResponseBuilder'
|
import ResponseBuilder from 'libs/ResponseBuilder'
|
||||||
import DaoFactory from 'models/DaoFactory'
|
import DaoFactory from 'models/DaoFactory'
|
||||||
@ -11,3 +12,13 @@ export const GET: APIRoute = async (ctx) => {
|
|||||||
.body((await dao.findOne({project: projectId, id: taskId})))
|
.body((await dao.findOne({project: projectId, id: taskId})))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
export const POST: APIRoute = async (ctx) => {
|
||||||
|
const taskId = ctx.params.issueId!
|
||||||
|
const dao = DaoFactory.get('issue')
|
||||||
|
const body = objectOmit(await ctx.request.json(), 'label')
|
||||||
|
const res = await dao.patch(taskId, {id: taskId, ...body})
|
||||||
|
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.body(res)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
31
src/pages/api/v1/projects/[id]/issues/[issueId]/labels.ts
Normal file
31
src/pages/api/v1/projects/[id]/issues/[issueId]/labels.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import ResponseBuilder from 'libs/ResponseBuilder'
|
||||||
|
import DaoFactory from 'models/DaoFactory'
|
||||||
|
|
||||||
|
export const POST: APIRoute = async (ctx) => {
|
||||||
|
const taskId = ctx.params.issueId!
|
||||||
|
const body = await ctx.request.json()
|
||||||
|
const newLabel = body.label
|
||||||
|
const dao = DaoFactory.get('issue')
|
||||||
|
const req = await dao.get(taskId)
|
||||||
|
req?.labels.push(newLabel)
|
||||||
|
const res = await dao.update(req!)
|
||||||
|
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.body(res)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
export const DELETE: APIRoute = async (ctx) => {
|
||||||
|
const taskId = ctx.params.issueId!
|
||||||
|
const body = await ctx.request.json()
|
||||||
|
const labelToRemove = body.label
|
||||||
|
const dao = DaoFactory.get('issue')
|
||||||
|
const req = await dao.get(taskId)!
|
||||||
|
console.log(req, taskId)
|
||||||
|
req!.labels.splice(req!.labels.indexOf(labelToRemove), 1)
|
||||||
|
const res = await dao.update(req!)
|
||||||
|
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.body(res)
|
||||||
|
.build()
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import type { APIRoute } from 'astro'
|
import type { APIRoute } from 'astro'
|
||||||
import ResponseBuilder from 'libs/ResponseBuilder'
|
import ResponseBuilder from 'libs/ResponseBuilder'
|
||||||
import DaoFactory from 'models/DaoFactory'
|
import DaoFactory from 'models/DaoFactory'
|
||||||
|
import { Sort } from 'models/Query'
|
||||||
|
|
||||||
export const POST: APIRoute = async (ctx) => {
|
export const POST: APIRoute = async (ctx) => {
|
||||||
const projectId = ctx.params.id!
|
const projectId = ctx.params.id!
|
||||||
@ -13,11 +14,10 @@ export const POST: APIRoute = async (ctx) => {
|
|||||||
project: projectId,
|
project: projectId,
|
||||||
preset: true
|
preset: true
|
||||||
})
|
})
|
||||||
console.log(issueCount)
|
|
||||||
const res = await dao.create({
|
const res = await dao.create({
|
||||||
...(await ctx.request.json()),
|
...(await ctx.request.json()),
|
||||||
project: projectId,
|
project: projectId,
|
||||||
localid: issueCount.rowsTotal + 1,
|
localid: issueCount.rows + 1,
|
||||||
state: defaultState?.id ?? 'empty',
|
state: defaultState?.id ?? 'empty',
|
||||||
labels: []
|
labels: []
|
||||||
})
|
})
|
||||||
@ -29,6 +29,6 @@ export const GET: APIRoute = async (ctx) => {
|
|||||||
const dao = DaoFactory.get('issue')
|
const dao = DaoFactory.get('issue')
|
||||||
|
|
||||||
return new ResponseBuilder()
|
return new ResponseBuilder()
|
||||||
.body((await dao.findAll({project: projectId})).data)
|
.body((await dao.findAll({project: projectId, $sort: { localid: Sort.ASC}})).data)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
const dao = DaoFactory.get('project')
|
const dao = DaoFactory.get('project')
|
||||||
const filters: Query<any> = {}
|
const filters: Query<any> = {}
|
||||||
url.searchParams.forEach((it, key) => {
|
url.searchParams.forEach((it, key) => {
|
||||||
filters[key] = it
|
filters[key] = { $inc: it }
|
||||||
})
|
})
|
||||||
const res = await dao.findAll(filters)
|
const res = await dao.findAll(filters)
|
||||||
return new ResponseBuilder().body(res.data).build()
|
return new ResponseBuilder().body(res.data).build()
|
||||||
|
@ -4,9 +4,24 @@ import Input from 'components/global/Input.astro'
|
|||||||
import MainLayout from 'layouts/MainLayout.astro'
|
import MainLayout from 'layouts/MainLayout.astro'
|
||||||
import DaoFactory from 'models/DaoFactory'
|
import DaoFactory from 'models/DaoFactory'
|
||||||
import Schema from 'models/Schema'
|
import Schema from 'models/Schema'
|
||||||
|
import type { StateObj } from 'models/State'
|
||||||
import route from 'route'
|
import route from 'route'
|
||||||
|
|
||||||
const dao = DaoFactory.get('project')
|
const dao = DaoFactory.get('project')
|
||||||
|
const stateDao = DaoFactory.get('state')
|
||||||
|
const defaultStates: Array<Partial<StateObj>> = [{
|
||||||
|
name: 'Todo',
|
||||||
|
color: '#123456',
|
||||||
|
preset: true
|
||||||
|
}, {
|
||||||
|
name: 'WIP',
|
||||||
|
color: '#123456',
|
||||||
|
preset: false
|
||||||
|
}, {
|
||||||
|
name: 'Done',
|
||||||
|
color: '#123456',
|
||||||
|
preset: false
|
||||||
|
}]
|
||||||
|
|
||||||
if (Astro.request.method === 'POST') {
|
if (Astro.request.method === 'POST') {
|
||||||
const input = new Schema({
|
const input = new Schema({
|
||||||
@ -16,9 +31,17 @@ if (Astro.request.method === 'POST') {
|
|||||||
const project = await dao.create({
|
const project = await dao.create({
|
||||||
name: input.name
|
name: input.name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return Astro.redirect('/?message=project+not+created')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const state of defaultStates) {
|
||||||
|
await stateDao.create({ ...state, project: project.id })
|
||||||
|
}
|
||||||
|
|
||||||
if (project) {
|
if (project) {
|
||||||
return Astro.redirect(route('/projects/[id]', {id: project.id, message: 'project created succefully'}))
|
return Astro.redirect(route('/projects/[id]', {id: project.id, message: 'project created succefully'}))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,10 +59,9 @@ if (Astro.request.method === 'POST') {
|
|||||||
</form>
|
</form>
|
||||||
<Input
|
<Input
|
||||||
name="name"
|
name="name"
|
||||||
data-action="/api/v1/projects"
|
data-input="/api/v1/projects"
|
||||||
data-template="#projectItem"
|
data-output="#projectItem ul inner"
|
||||||
data-trigger="keydown load after:100"
|
data-trigger="keydown load after:100"
|
||||||
data-target="innerHTML ul"
|
|
||||||
data-multiple
|
data-multiple
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -4,6 +4,31 @@ import Button from 'components/global/Button.astro'
|
|||||||
import DaoFactory from 'models/DaoFactory'
|
import DaoFactory from 'models/DaoFactory'
|
||||||
import route from 'route'
|
import route from 'route'
|
||||||
import Input from 'components/global/Input.astro'
|
import Input from 'components/global/Input.astro'
|
||||||
|
import Table from 'components/global/Table/Table.astro'
|
||||||
|
import Select from 'components/global/Select.astro'
|
||||||
|
import { X } from 'lucide-astro'
|
||||||
|
import { Sort } from 'models/Query'
|
||||||
|
import type { StateObj } from 'models/State'
|
||||||
|
|
||||||
|
const defaultStates: Array<StateObj> = [{
|
||||||
|
id: '',
|
||||||
|
name: 'Todo',
|
||||||
|
project: '',
|
||||||
|
color: '#123456',
|
||||||
|
preset: true
|
||||||
|
}, {
|
||||||
|
id: '',
|
||||||
|
name: 'WIP',
|
||||||
|
project: '',
|
||||||
|
color: '#123456',
|
||||||
|
preset: false
|
||||||
|
}, {
|
||||||
|
id: '',
|
||||||
|
name: 'Done',
|
||||||
|
project: '',
|
||||||
|
color: '#123456',
|
||||||
|
preset: false
|
||||||
|
}]
|
||||||
|
|
||||||
const id = Astro.params.id!
|
const id = Astro.params.id!
|
||||||
const project = await DaoFactory.get('project').findById(id)
|
const project = await DaoFactory.get('project').findById(id)
|
||||||
@ -12,22 +37,51 @@ if (!project) {
|
|||||||
return Astro.rewrite(route('/404'))
|
return Astro.rewrite(route('/404'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = await DaoFactory.get('issue').findAll({
|
const dbStates = await DaoFactory.get('state').findAll({ project: project.id, $sort: { preset: Sort.DESC } })
|
||||||
project: project.id
|
console.log(dbStates)
|
||||||
})
|
const states = dbStates.data.length > 0 ? dbStates.data : defaultStates
|
||||||
|
console.log(project, states)
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<main class="container flex flex-col gap-24 justify-center items-center md:mt-6">
|
<main class="container flex gap-24 justify-center items-center md:mt-6">
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
name="displayID"
|
||||||
|
value={project.displayid}
|
||||||
|
data-trigger="input after:500"
|
||||||
|
data-input={`post:${route('/api/v1/projects/[id]', {id: project.id})}`}
|
||||||
|
data-output="run:reload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-6xl text-center font-bold">{project.name}</h1>
|
<h1 class="text-6xl text-center font-bold">{project.name}</h1>
|
||||||
<ul
|
<Table>
|
||||||
data-action={route('/api/v1/projects/[id]/issues', {id: project.id})}
|
<thead>
|
||||||
data-multiple
|
<tr>
|
||||||
data-template="template#issue"
|
<th>id</th>
|
||||||
>{tasks.data.map((it) => (<li>{project.displayID}-{it.localid} {it.name}</li>))}</ul>
|
<th>name</th>
|
||||||
<template id="issue"><li data-template="#issueTemplate" data-target="innerHTML #issue-details" data-attribute={`'${project.displayID}-{localid} {name}' data-action:${route('/api/v1/projects/[id]/issues/[issueId]', {id: project.id, issueId: '{id}'}, true)}`}></li></template>
|
</tr>
|
||||||
<form data-action={route('/api/v1/projects/[id]/issues', {id: project.id})} data-method="POST" data-run="ul[data-action]">
|
</thead>
|
||||||
|
<tbody
|
||||||
|
data-input={route('/api/v1/projects/[id]/issues', {id: project.id})}
|
||||||
|
data-multiple
|
||||||
|
data-trigger="load"
|
||||||
|
data-output="template#issue"
|
||||||
|
></tbody>
|
||||||
|
</Table>
|
||||||
|
<template id="issue">
|
||||||
|
<tr
|
||||||
|
class="cursor-pointer"
|
||||||
|
data-output="#issueTemplate #issue-details inner"
|
||||||
|
data-input={route('/api/v1/projects/[id]/issues/[issueId]', {id: project.id, issueId: '{id}'}, true)}
|
||||||
|
data-attribute="data-id:{id}"
|
||||||
|
>
|
||||||
|
<td data-attribute={`${project.displayid}-{localid}`}></td>
|
||||||
|
<td data-attribute="name"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<form data-input={`post:${route('/api/v1/projects/[id]/issues', {id: project.id})}`} data-output="hyp:tbody[data-input]">
|
||||||
<Input name="name" label="Ajouter une Issue" placeholder="nom de la tache" />
|
<Input name="name" label="Ajouter une Issue" placeholder="nom de la tache" />
|
||||||
<Button>Ajouter</Button>
|
<Button>Ajouter</Button>
|
||||||
</form>
|
</form>
|
||||||
@ -35,11 +89,32 @@ const tasks = await DaoFactory.get('issue').findAll({
|
|||||||
<div>
|
<div>
|
||||||
<div id="issue-details"></div>
|
<div id="issue-details"></div>
|
||||||
<template id="issueTemplate">
|
<template id="issueTemplate">
|
||||||
<h2 data-attribute="name"></h2>
|
<form
|
||||||
<ul data-loop="labels">
|
data-trigger="keyup pointerup after:250"
|
||||||
<li data-attribute="this"></li>
|
data-input={`post:${route('/api/v1/projects/[id]/issues/[issueId]', {id: project.id, issueId: '{id}'}, true)}`}
|
||||||
</ul>
|
>
|
||||||
<p data-attribute="description"></p>
|
<h2 data-attribute="name"></h2>
|
||||||
|
<Select label="progress" name="state" data-attribute="value:state" options={states.map((state) => ({ value: state.id, title: state.name}))} />
|
||||||
|
<ul>
|
||||||
|
<li data-loop="labels">
|
||||||
|
<span data-attribute="this"></span>
|
||||||
|
<X
|
||||||
|
data-params="label:{this}"
|
||||||
|
data-input={`delete:${route('/api/v1/projects/[id]/issues/[issueId]/labels', {id: project.id, issueId: '{id}'}, true)}`}
|
||||||
|
data-output="hyp:[data-id='{id}']"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li><Select
|
||||||
|
data-trigger="change"
|
||||||
|
name="label"
|
||||||
|
options={['a', 'b', 'c']}
|
||||||
|
data-output="hyp:[data-id='{id}']"
|
||||||
|
data-input={`post:${route('/api/v1/projects/[id]/issues/[issueId]/labels', {id: project.id, issueId: '{id}'}, true)}`}
|
||||||
|
/></li>
|
||||||
|
</ul>
|
||||||
|
<Input type="textarea" name="description" data-attribute="description"></Input>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user