feat: Add multi selector

Signed-off-by: Avior <github@avior.me>
This commit is contained in:
Florian Bouillon 2024-06-13 23:56:34 +02:00
parent 89d2debb1e
commit d8ad16608e
7 changed files with 128 additions and 90 deletions

View File

@ -52,7 +52,7 @@ if (baseProps.type === '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 text-black @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 m-0 p-0 border-0
} }
.textarea { .textarea {
@apply overflow-y-hidden @apply overflow-y-hidden

View File

@ -7,14 +7,16 @@ export interface Props extends Omit<astroHTML.JSX.InputHTMLAttributes, 'type'> {
block?: boolean block?: boolean
suffix?: string suffix?: string
prefix?: string prefix?: string
multiple?: boolean
options: Array<string | number | {value?: string, 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')
const values = Array.isArray(Astro.props.value) ? Astro.props.value : (Astro.props.value?.toString()?.split(',') ?? [])
--- ---
<!-- input wrapper --> <!-- input wrapper -->
<label data-component="select" data-options={Astro.props.options} class:list={['parent', {'w-full': Astro.props.block}]}> <label data-component="select" data-options={JSON.stringify(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,19 +25,23 @@ 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 type="hidden" {...baseProps} />
<input readonly {...objectOmit(baseProps, 'name') as any} /> <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}
} }
const itemValue = it.value ?? it.title
const checked = values.includes(itemValue)
return ( return (
<li data-value={it.value ?? it.title}> <li>
<label class="flex gap-2">
<input width={14} height={14} hidden={!Astro.props.multiple} type={Astro.props.multiple ? 'checkbox' : 'radio'} name={baseProps.name} value={itemValue} checked={checked} />
<p>{it.title}</p> <p>{it.title}</p>
{it.description && ( {it.description && (
<p class="desc">{it.description}</p> <p class="desc">{it.description}</p>
)} )}
</label>
</li> </li>
) )
})} })}
@ -51,10 +57,6 @@ const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix',
@apply flex flex-col cursor-text gap-2 @apply flex flex-col cursor-text gap-2
} }
.parent input, .parent textarea {
@apply w-full
}
.suffix, .prefix { .suffix, .prefix {
@apply select-none font-light text-gray-400 @apply select-none font-light text-gray-400
} }
@ -62,8 +64,8 @@ const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix',
@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 > input {
@apply bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none text-black @apply w-full bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none text-black
} }
.textarea { .textarea {
@ -73,7 +75,7 @@ const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix',
.list { .list {
@apply absolute top-full mt-2 z-10 bg-gray-50 rounded-lg border 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, ul:hover { .input > input:focus + ul, ul:hover {
@apply block @apply block
} }
ul :global(li) { ul :global(li) {
@ -93,28 +95,29 @@ const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix',
Component.addComponent<HTMLElement>('select', (it) => { Component.addComponent<HTMLElement>('select', (it) => {
const input = it.querySelector<HTMLInputElement>('input[type="hidden"]')
const displayInput = it.querySelector<HTMLInputElement>('input[readonly]') const displayInput = it.querySelector<HTMLInputElement>('input[readonly]')
const inputs = Array.from(it.querySelectorAll<HTMLInputElement>('li input'))
const list = it.querySelector('ul') const list = it.querySelector('ul')
if (!input || !list || !displayInput) { if (!list || !displayInput) {
return return
} }
function setSelectValue(value: string, displayValue: string) { function updateSelectValue() {
input!.value = value const checkedValues = inputs.filter((it) => it.checked).map((it) => it.value)
input!.setAttribute('value', value) displayInput.value = checkedValues.toString()
displayInput!.value = displayValue
input!.dispatchEvent(new Event('change'))
} }
const value = input.getAttribute('value') ?? displayInput.getAttribute('value') const values = displayInput.getAttribute('value')?.split(',') ?? []
if (value) { console.log('values', values)
const displayValue = list.querySelector(`li[data-value="${value}"]`) if (values.length > 0) {
if (displayValue) { const checkbox = list.querySelector(`input[value="${value}"]`)
setSelectValue(value!, displayValue.querySelector('p')?.innerText ?? '') if (checkbox) {
checkbox.checked = true
} }
} }
list.querySelectorAll('li').forEach((listItem) => { list.querySelectorAll('li input').forEach((listItem: HTMLInputElement) => {
listItem.addEventListener('click', () => { console.log('')
setSelectValue(listItem.dataset.value!, listItem.querySelector('p')?.innerText ?? '') listItem.addEventListener('change', () => {
console.log(listItem, 'changed to', listItem.checked)
updateSelectValue()
}) })
}) })
}) })

View File

@ -1,13 +1,6 @@
import Priority from 'models/Priority'
import Schema, { type Impl } from 'models/Schema' import Schema, { type Impl } from 'models/Schema'
export enum Priority {
None,
Low,
Medium,
High,
Urgent
}
const schema = new Schema({ const schema = new Schema({
/** /**
* the project ID * the project ID
@ -24,7 +17,7 @@ const schema = new Schema({
description: { type: String, nullable: true }, description: { type: String, nullable: true },
state: String, //state id state: String, //state id
priority: {type: Number, defaultValue: Priority.None}, priority: {type: Number, defaultValue: Priority.NONE},
begin: { type: Date, nullable: true }, begin: { type: Date, nullable: true },
due: { type: Date, nullable: true }, due: { type: Date, nullable: true },

View File

@ -0,0 +1,23 @@
enum Priority {
NONE,
LOW,
MEDIUM,
HIGH,
URGENT,
}
export function getPriorityText(priority: Priority) {
switch (priority) {
case Priority.NONE: return 'None'
case Priority.LOW: return 'Low'
case Priority.MEDIUM: return 'Medium'
case Priority.HIGH: return 'High'
case Priority.URGENT: return 'Urgent'
}
}
export function getPriorities(): Array<Priority> {
return Object.values(Priority).filter((it) => typeof it === 'number') as Array<Priority>
}
export default Priority

View File

@ -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 Priority, { getPriorityText } from 'models/Priority'
import { Sort } from 'models/Query' import { Sort } from 'models/Query'
export const POST: APIRoute = async (ctx) => { export const POST: APIRoute = async (ctx) => {
@ -26,9 +27,18 @@ export const POST: APIRoute = async (ctx) => {
export const GET: APIRoute = async (ctx) => { export const GET: APIRoute = async (ctx) => {
const projectId = ctx.params.id! const projectId = ctx.params.id!
const sort = ctx.params.sort ?? 'localid'
const dao = DaoFactory.get('issue') const dao = DaoFactory.get('issue')
const stateDao = DaoFactory.get('state')
const states = (await stateDao.findAll({ project: projectId })).data
const issues = (await dao.findAll({project: projectId, $sort: { [sort]: Sort.ASC}})).data
const res = issues.map((it) => ({
...it,
state: states.find((st) => st.id === it.state),
priority: getPriorityText(it.priority ?? Priority.NONE)
}))
return new ResponseBuilder() return new ResponseBuilder()
.body((await dao.findAll({project: projectId, $sort: { localid: Sort.ASC}})).data) .body(res)
.build() .build()
} }

View File

@ -1,6 +1,7 @@
--- ---
import Button from 'components/global/Button.astro' import Button from 'components/global/Button.astro'
import Input from 'components/global/Input.astro' import Input from 'components/global/Input.astro'
import Select from 'components/global/Select/index.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'
@ -47,6 +48,8 @@ if (Astro.request.method === 'POST') {
return Astro.redirect(route('/', {message: 'Error creating your project'})) return Astro.redirect(route('/', {message: 'Error creating your project'}))
} }
const projects = await dao.findAll()
--- ---
<MainLayout title="Dzeio - Website Template"> <MainLayout title="Dzeio - Website Template">
@ -57,16 +60,20 @@ if (Astro.request.method === 'POST') {
<Input label="Nom du projet" name="name" /> <Input label="Nom du projet" name="name" />
<Button>Créer</Button> <Button>Créer</Button>
</form> </form>
<Input <form action="">
<Select
multiple
options={projects.data.map((it) => ({value: it.id, title: it.name}))}
name="name" name="name"
data-input="/api/v1/projects" data-input="/api/v1/projects"
data-output="#projectItem ul inner" data-output="#projectItem ul#results inner"
data-trigger="keydown load after:100" data-trigger="keydown load after:100"
data-multiple data-multiple
/> />
</form>
<p>Results</p> <p>Results</p>
<ul> <ul id="results">
</ul> </ul>
</main> </main>
@ -75,27 +82,6 @@ if (Astro.request.method === 'POST') {
<a data-attribute="name href:/projects/{id}"></a> <a data-attribute="name href:/projects/{id}"></a>
</li> </li>
</template> </template>
<!-- Define a template which contains how it is displayed -->
<template id="pouet">
<div>
<!-- Define the attribute in the object that will be assigned -->
<p data-attribute="caca"></p>
<!-- You can change the display type by selecting `html` | `innerHTML` | `outerHTML` | `text` | `innerText` | `outerText` -->
<p data-attribute="caca" data-type="html"></p>
<!-- You can even add another Requester inside the template! -->
<p
data-attribute="caca value:caca"
data-action="/api/v1/checkout"
data-template="#pouet"
data-trigger="mouseover"
data-target="outerHTML"
></p>
</div>
</template>
<template id="list">
<li data-attribute="this"></li>
</template>
</MainLayout> </MainLayout>
<script> <script>

View File

@ -5,10 +5,11 @@ 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 Table from 'components/global/Table/Table.astro'
import Select from 'components/global/Select.astro' import Select from 'components/global/Select/index.astro'
import { X } from 'lucide-astro' import { X } from 'lucide-astro'
import { Sort } from 'models/Query' import { Sort } from 'models/Query'
import type { StateObj } from 'models/State' import type { StateObj } from 'models/State'
import Priority, { getPriorities, getPriorityText } from 'models/Priority'
const defaultStates: Array<StateObj> = [{ const defaultStates: Array<StateObj> = [{
id: '', id: '',
@ -45,7 +46,7 @@ console.log(project, states)
<MainLayout> <MainLayout>
<main class="container flex gap-24 justify-center items-center md:mt-6"> <main class="container flex gap-24 justify-center items-center md:mt-6">
<div> <!-- <div class="w-1/3">
<Input <Input
name="displayID" name="displayID"
value={project.displayid} value={project.displayid}
@ -53,14 +54,18 @@ console.log(project, states)
data-input={`post:${route('/api/v1/projects/[id]', {id: project.id})}`} data-input={`post:${route('/api/v1/projects/[id]', {id: project.id})}`}
data-output="run:reload" data-output="run:reload"
/> />
</div> </div> -->
<div> <div class="w-1/2">
<h1 class="text-6xl text-center font-bold">{project.name}</h1> <h1 class="text-6xl text-center font-bold">{project.name}</h1>
<Select data-trigger="change" data-output="hyp:tbody[data-input]" name="sort" options={[{value: 'localid', title: 'ID'}, {value: 'state', title: 'State'}]} />
<Table> <Table>
<thead> <thead>
<tr> <tr>
<th>id</th> <th>id</th>
<th>name</th> <th>name</th>
<th>state</th>
<th>Priority</th>
<th>Labels</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
@ -79,6 +84,15 @@ console.log(project, states)
> >
<td data-attribute={`${project.displayid}-{localid}`}></td> <td data-attribute={`${project.displayid}-{localid}`}></td>
<td data-attribute="name"></td> <td data-attribute="name"></td>
<td data-attribute="state.name"></td>
<td data-attribute="priority"></td>
<td>
<ul class="flex gap-2">
<li data-loop="labels" data-attribute="this">
</li>
</ul>
</td>
</tr> </tr>
</template> </template>
<form data-input={`post:${route('/api/v1/projects/[id]/issues', {id: project.id})}`} data-output="hyp:tbody[data-input]"> <form data-input={`post:${route('/api/v1/projects/[id]/issues', {id: project.id})}`} data-output="hyp:tbody[data-input]">
@ -86,34 +100,43 @@ console.log(project, states)
<Button>Ajouter</Button> <Button>Ajouter</Button>
</form> </form>
</div> </div>
<div> <div class="w-1/2">
<div id="issue-details"></div> <div id="issue-details"></div>
<template id="issueTemplate"> <template id="issueTemplate">
<form <form
data-trigger="keyup pointerup after:250" data-trigger="keyup pointerup after:250"
data-input={`post:${route('/api/v1/projects/[id]/issues/[issueId]', {id: project.id, issueId: '{id}'}, true)}`} data-input={`post:${route('/api/v1/projects/[id]/issues/[issueId]', {id: project.id, issueId: '{id}'}, true)}`}
data-output="hyp:tbody[data-input]"
class="flex flex-col gap-5"
> >
<h2 data-attribute="name"></h2> <Input data-attribute="value:name" name="name" />
<Select label="progress" name="state" data-attribute="value:state" options={states.map((state) => ({ value: state.id, title: state.name}))} /> <Input type="textarea" name="description" data-attribute="description"></Input>
<ul>
<li data-loop="labels"> <div class="flex flex-col gap-4">
<div class="flex gap-4 items-center"><p>State</p><Select name="state" data-attribute="value:state" options={states.map((state) => ({ value: state.id, title: state.name}))} /></div>
<div class="flex gap-4 items-center"><p>Priority</p><Select name="priority" data-attribute="value:priority" options={getPriorities().map((priority: Priority) => ({ value: priority, title: getPriorityText(priority)}))} /></div>
<div class="flex gap-4 items-center"><p>Labels</p><Select
data-trigger="change"
name="label"
multiple
options={['a', 'b', 'c']}
data-output="hyp:[data-id='{id}']"
data-attribute="value:labels"
data-input={`post:${route('/api/v1/projects/[id]/issues/[issueId]/labels', {id: project.id, issueId: '{id}'}, true)}`}
/></div>
<ul class="flex gap-2 flex-row">
<li data-loop="labels" class="group flex gap-2 bg-slate-700 px-2 py-px rounded-full items-center">
<span data-attribute="this"></span> <span data-attribute="this"></span>
<X <X
class="opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
data-params="label:{this}" data-params="label:{this}"
data-input={`delete:${route('/api/v1/projects/[id]/issues/[issueId]/labels', {id: project.id, issueId: '{id}'}, true)}`} data-input={`delete:${route('/api/v1/projects/[id]/issues/[issueId]/labels', {id: project.id, issueId: '{id}'}, true)}`}
data-output="hyp:[data-id='{id}']" data-output="hyp:[data-id='{id}']"
/> />
</li> </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> </ul>
<Input type="textarea" name="description" data-attribute="description"></Input> </div>
<button type="submit">Submit</button>
</form> </form>
</template> </template>
</div> </div>