1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-01-18 13:07:58 +00:00

Table view

This commit is contained in:
Romein van Buren 2023-01-31 16:58:23 +01:00
parent dc5b12ee0d
commit 14e5b1f806
Signed by: romein
GPG Key ID: 0EFF8478ADDF6C49
20 changed files with 674 additions and 172 deletions

View File

@ -1,4 +1,4 @@
import { int32, int64, isInt, uint64 } from './utils';
import { canBeObjectId, int32, int64, isInt, uint64 } from './utils';
export function input(node, { autofocus, type, onValid, onInvalid, mandatory } = {
autofocus: false,
@ -47,6 +47,9 @@ export function input(node, { autofocus, type, onValid, onInvalid, mandatory } =
}
return false;
case 'objectid':
return !canBeObjectId(node.value) && 'Invalid string representation of an ObjectId';
case 'double':
case 'decimal':
default:

View File

@ -0,0 +1,70 @@
<script>
export let date = new Date();
$: hours = date.getHours();
$: minutes = date.getMinutes();
$: seconds = date.getSeconds();
</script>
<svg viewBox="-50 -50 100 100" class="clock">
<circle class="clock-face" r="48" />
<!-- markers -->
{#each [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 ] as minute}
<line class="major" y1="35" y2="45" transform="rotate({30 * minute})" />
{#each [ 1, 2, 3, 4 ] as offset}
<line class="minor" y1="42" y2="45" transform="rotate({6 * (minute + offset)})" />
{/each}
{/each}
<!-- hour hand -->
<line class="hour" y1="2" y2="-20" transform="rotate({30 * hours + 0.5 * minutes})" />
<!-- minute hand -->
<line class="minute" y1="4" y2="-30" transform="rotate({6 * minutes + 0.1 * seconds})" />
<!-- second hand -->
<g transform="rotate({6 * seconds})">
<line class="second" y1="10" y2="-38" />
<line class="second-counterweight" y1="10" y2="2" />
</g>
</svg>
<style>
.clock {
width: 100%;
height: 100%;
}
.clock-face {
stroke: #333;
fill: white;
}
.minor {
stroke: #999;
stroke-width: 0.5;
}
.major {
stroke: #333;
stroke-width: 1;
}
.hour {
stroke: #333;
}
.minute {
stroke: #666;
}
.second, .second-counterweight {
stroke: rgb(180, 0, 0);
}
.second-counterweight {
stroke-width: 3;
}
</style>

View File

@ -0,0 +1,201 @@
<script>
import { addDays, getWeek, isDate, isSameDay, startOfWeek } from 'date-fns';
import { onMount } from 'svelte';
import { daysAbbr, months } from '../utils';
import Clock from './clock.svelte';
import Icon from './icon.svelte';
import Modal from './modal.svelte';
export let value;
export let show = false;
const rows = [ ...Array(6).keys() ];
const cols = [ ...Array(7).keys() ];
const now = new Date();
let year = now.getFullYear();
let month = now.getMonth();
let day = now.getDate();
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();
let calendar = [];
onMount(() => setDateToValues(now));
$: setValueToDate(year, month, day, hour, minute, second);
$: setDateToValues(value);
function buildCalendar(y = year, m = month) {
const date = new Date(y, m);
let curDate = startOfWeek(date, { weekStartsOn: 1 });
return rows.map(() => {
const week = [];
cols.forEach(() => {
week.push(curDate);
curDate = addDays(curDate, 1);
});
return week;
});
}
function setValueToDate(date) {
if (isDate(date)) {
year = date.getFullYear();
month = date.getMonth();
day = date.getDate();
hour = date.getHours();
minute = date.getMinutes();
value = new Date(year, month, day, hour, minute, second);
}
else {
if (hour < 0) {
day--; hour = 23;
}
else if (hour > 23) {
day++; hour = 0;
}
else if (minute < 0) {
hour--; minute = 59;
}
else if (minute > 59) {
hour++; minute = 0;
}
else if (second < 0) {
minute--; second = 59;
}
else if (second > 59) {
minute++; second = 0;
}
value = new Date(year, month, day, hour, minute, second);
}
calendar = buildCalendar(year, month);
}
function setDateToValues(date) {
year = date.getFullYear();
month = date.getMonth();
day = date.getDate();
hour = date.getHours();
minute = date.getMinutes();
second = date.getSeconds();
calendar = buildCalendar(year, month);
}
</script>
<Modal width="700px" bind:show>
<div class="datepicker">
<div class="date">
<div class="field">
<input type="number" bind:value={day} />
<select bind:value={month}>
{#each months as monthName, index}
<option value={index}>{monthName}</option>
{/each}
</select>
<input type="number" bind:value={year} />
</div>
<table class="calendar">
<thead>
<tr>
<th></th>
{#each daysAbbr as dayName}
<th>{dayName}</th>
{/each}
</tr>
</thead>
{#each calendar as week}
<tr>
<td class="week">{getWeek(week[0])}</td>
{#each week as day}
<td class="day">
<button
on:click={() => setValueToDate(day)}
type="button"
class="btn-sm"
class:active={isSameDay(value, day)}
class:notinmonth={day.getMonth() !== month}
>{day.getDate()}</button>
</td>
{/each}
</tr>
{/each}
</table>
</div>
<div class="time">
<div class="field">
<input type="number" bind:value={hour} placeholder="hours" />
<span class="label">:</span>
<input type="number" bind:value={minute} placeholder="mins" />
<span class="label">:</span>
<input type="number" bind:value={second} placeholder="secs" />
</div>
<Clock date={value} />
</div>
</div>
<div slot="footer" class="footer">
<button class="btn secondary" type="button" on:click={() => value = new Date()}>
<Icon name="o" /> Set to now
</button>
<button class="btn" type="button" on:click={() => show = false}>
<Icon name="check" /> OK
</button>
</div>
</Modal>
<style>
.datepicker {
display: grid;
grid-template: 1fr / 1fr 1fr;
gap: 0.5rem;
}
.calendar {
width: 100%;
text-align: center;
}
.calendar thead th {
opacity: 0.5;
padding-top: 8px;
padding-bottom: 8px;
}
.calendar .week {
text-align: right;
opacity: 0.5;
padding-right: 10px;
}
.calendar .day button {
display: block;
width: 100%;
padding-top: 8px;
padding-bottom: 8px;
}
.calendar .day button.active {
background-color: #00008b;
color: #fff;
}
.calendar .day button.notinmonth {
opacity: 0.6;
}
.time {
display: flex;
flex-direction: column;
}
.time input {
text-align: center;
}
.time :global(.clock) {
height: 150px;
width: 150px;
margin: auto;
}
.footer {
display: flex;
gap: 0.5rem;
justify-content: right;
}
</style>

View File

@ -0,0 +1,136 @@
<script>
import { numericInputTypes } from '../utils';
import { input } from '../actions';
import Icon from './icon.svelte';
import { ObjectId } from 'bson';
import Datepicker from './datepicker.svelte';
export let column = {};
export let value = undefined;
export let valid = true;
const onValid = () => valid = true;
const onInvalid = () => valid = false;
let objectIdInput;
let showDatepicker;
$: type = column.inputType;
$: mandatory = column.mandatory;
$: if ((value === undefined) && mandatory) {
valid = false;
}
function markInputValid(input) {
input.setCustomValidity('');
input.reportValidity();
input.classList.remove('invalid');
valid = true;
}
function setObjectId(event) {
if (valid) {
value = new ObjectId(event.currentTarget?.value);
}
}
function generateObjectId() {
value = new ObjectId();
objectIdInput.value = value.toString();
markInputValid(objectIdInput);
objectIdInput.disabled = true;
}
function editObjectId() {
if (!objectIdInput) {
return;
}
objectIdInput.disabled = false;
objectIdInput.focus();
}
function selectChange() {
if ((value === undefined) && mandatory) {
valid = false;
}
else {
valid = true;
}
}
</script>
<div class="forminput {type}">
<div class="field">
{#if type === 'string'}
<input type="text" bind:value use:input={{ type, onValid, onInvalid, mandatory }} />
{:else if type === 'objectid'}
<input
type="text"
bind:this={objectIdInput}
on:input={setObjectId}
use:input={{ type, onValid, onInvalid, mandatory }}
/>
{:else if numericInputTypes.includes(type)}
<input type="number" bind:value use:input={{ type, onValid, onInvalid, mandatory }} />
{:else if type === 'bool'}
<select bind:value on:change={selectChange}>
<option value={undefined} disabled={mandatory}>Unset</option>
<option value={true}>Yes / true</option>
<option value={false}>No / false</option>
</select>
{:else if type === 'date'}
<input type="text" readonly value={value?.toString() || '...'} on:focus={() => showDatepicker = true} />
{/if}
</div>
<div class="actions">
{#if type === 'objectid'}
{#if objectIdInput?.disabled}
<button class="btn-sm" type="button" title="Edit object id" on:click={editObjectId}>
<Icon name="edit" />
</button>
{/if}
<button class="btn-sm" type="button" title="Generate random object id" on:click={generateObjectId}>
<Icon name="reload" />
</button>
{:else if type === 'date'}
<button class="btn-sm" type="button" title="Edit date" on:click={() => showDatepicker = true}>
<Icon name="edit" />
</button>
<button class="btn-sm" type="button" title="Set date to now" on:click={() => value = new Date()}>
<Icon name="o" />
</button>
{/if}
<button class="btn-sm" type="button" title="Reset field to default value" on:click={() => value = undefined}>
<Icon name="trash" />
</button>
</div>
</div>
{#if type === 'date'}
<Datepicker bind:value bind:show={showDatepicker} />
{/if}
<style>
.forminput {
position: relative;
}
.forminput.date input {
cursor: pointer;
}
.actions {
display: none;
position: absolute;
right: 5px;
top: 6px;
background-color: #fff;
}
.actions button:last-child {
border-radius: 2px 6px 6px 2px;
}
.forminput:hover .actions {
display: flex;
}
</style>

View File

@ -2,7 +2,8 @@
import { contextMenu } from '../stores';
import { createEventDispatcher } from 'svelte';
import Icon from './icon.svelte';
import { resolveKeypath } from '../utils';
import { resolveKeypath, setValue } from '../utils';
import FormInput from './forminput.svelte';
export let items = [];
export let columns = [];
@ -14,18 +15,34 @@
export let striped = true;
export let hideObjectIndicators = false;
export let hideChildrenToggles = false;
export let canSelect = true;
export let canRemoveItems = false;
export let inputsValid = false;
const dispatch = createEventDispatcher();
const keypathProxies = {};
const validity = {};
let childrenOpen = {};
let _items = [];
$: refresh(hideObjectIndicators, items);
$: inputsValid = Object.values(validity).every(v => !!v);
function refresh(hideObjectIndicators, items) {
_items = objectToArray(items).map(item => {
item.children = objectToArray(item.children);
return item;
});
for (let index = 0; index < _items.length; index++) {
keypathProxies[index] = new Proxy(_items, {
get: (_items, key) => resolveKeypath(_items[index], key),
set: (_items, key, value) => {
setValue(_items[index], key, value);
return true;
},
});
}
}
function objectToArray(obj) {
@ -41,6 +58,10 @@
}
function select(itemKey) {
if (!canSelect) {
return false;
}
if (activeKey !== itemKey) {
activeKey = itemKey;
if (level === 0) {
@ -75,6 +96,11 @@
contextMenu.show(evt, item.menu);
}
function removeItem(index) {
items.splice(index, 1);
items = items;
}
function formatValue(value) {
if (Array.isArray(value)) {
return hideObjectIndicators ? '' : '[...]';
@ -98,12 +124,13 @@
}
</script>
{#each _items as item}
{#each _items as item, index}
<tr
on:click={() => select(item[key])}
on:dblclick={() => doubleClick(item[key])}
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
class:selected={!activePath[level + 1] && activePath.every(k => path.includes(k) || k === item[key]) && (activePath[level] === item[key])}
class:selectable={canSelect}
class:selected={canSelect && !activePath[level + 1] && activePath.every(k => path.includes(k) || k === item[key]) && (activePath[level] === item[key])}
class:striped
>
{#if !hideChildrenToggles}
@ -127,21 +154,35 @@
</td>
{#each columns as column, columnIndex}
{@const value = column.key?.includes('.') ? resolveKeypath(item, column.key) : item[column.key]}
<td class:right={column.right} title={value}>
<div class="value" style:margin-left="{level * 10}px">
{formatValue(value)}
</div>
<td class:right={column.right} title={keypathProxies[index][column.key]}>
{#if column.inputType}
<FormInput {column} bind:value={keypathProxies[index][column.key]} bind:valid={validity[columnIndex]} />
{:else}
<div class="value" style:margin-left="{level * 10}px">
{formatValue(keypathProxies[index][column.key])}
</div>
{/if}
</td>
{/each}
{#if canRemoveItems}
<td class="has-button">
<button class="btn-sm" type="button" on:click={() => removeItem(index)}>
<Icon name="x" />
</button>
</td>
{/if}
</tr>
{#if item.children && childrenOpen[item[key]]}
<svelte:self
{columns}
{hideObjectIndicators}
{key}
{striped}
{hideObjectIndicators}
{hideChildrenToggles}
{canSelect}
{canRemoveItems}
path={[ ...path, item[key] ]}
items={item.children}
level={level + 1}
@ -157,7 +198,10 @@
tr.striped:nth-of-type(even) td {
background-color: #eee;
}
tr.selected td {
tr.selectable {
cursor: pointer;
}
tr.selectable.selected td {
background-color: #00008b !important;
color: #fff;
}
@ -165,7 +209,6 @@
td {
padding: 2px;
text-overflow: ellipsis;
cursor: pointer;
}
td.has-toggle {
width: 20px;

View File

@ -1,16 +1,27 @@
<script>
import { createEventDispatcher } from 'svelte';
import GridItems from './grid-items.svelte';
// import Icon from './icon.svelte';
import Icon from './icon.svelte';
export let columns = [];
export let items = [];
// export let actions = [];
export let key = 'id';
export let activePath = [];
export let striped = true;
export let showHeaders = false;
export let hideObjectIndicators = false;
export let hideChildrenToggles = false;
export let canAddRows = false;
export let canSelect = true;
export let canRemoveItems = false;
export let inputsValid = false;
// export let actions = [];
const dispatch = createEventDispatcher();
function addRow() {
dispatch('addRow');
}
</script>
<div class="grid">
@ -38,6 +49,10 @@
{#each columns as column}
<th scope="col">{column.title || ''}</th>
{/each}
{#if canRemoveItems}
<th class="has-button"></th>
{/if}
</tr>
</thead>
{/if}
@ -48,13 +63,24 @@
{columns}
{key}
{striped}
{canSelect}
{canRemoveItems}
{hideObjectIndicators}
{hideChildrenToggles}
bind:activePath
bind:inputsValid
on:select
on:trigger
/>
</tbody>
{#if canAddRows}
<tfoot>
<button class="btn-sm" type="button" on:click={addRow}>
<Icon name="+" />
</button>
</tfoot>
{/if}
</table>
</div>
@ -86,8 +112,10 @@
th {
font-weight: 600;
text-align: left;
}
th {
padding: 2px;
}
/* tfoot button {
margin-top: 0.5rem;
} */
</style>

View File

@ -73,5 +73,11 @@
<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/>
{:else if name === 'target'}
<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>
{:else if name === 'trash'}
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/>
{:else if name === 'anchor'}
<circle cx="12" cy="5" r="3"/><path d="M12 22V8M5 12H2a10 10 0 0 0 20 0h-3"/>
{:else if name === 'o'}
<circle cx="12" cy="12" r="10"/>
{/if}
</svg>

View File

@ -1,5 +1,6 @@
<script>
import { isBsonBuiltin, isDate } from '../utils';
import { isDate } from 'date-fns';
import { isBsonBuiltin } from '../utils';
import Grid from './grid.svelte';
export let data = [];

View File

@ -8,41 +8,42 @@
export let kp = '';
const collapsedSymbol = '...';
const getType = i => {
if (i === null) {
return 'null';
}
return typeof i;
};
let displayOnly = true;
let items;
let isArray;
let openBracket;
let closeBracket;
$: {
items = getType(data) === 'object' ? Object.keys(data) : [];
isArray = Array.isArray(data);
openBracket = isArray ? '[' : '{';
closeBracket = isArray ? ']' : '}';
let collapsed;
let invalid = false;
let textarea;
$: items = getType(data) === 'object' ? Object.keys(data) : [];
$: isArray = Array.isArray(data);
$: openBracket = isArray ? '[' : '{';
$: closeBracket = isArray ? ']' : '}';
$: collapsed = depth < level;
$: textarea && resizeTextarea();
function getType(value) {
if (value === null) {
return 'null';
}
return typeof value;
}
let collapsed;
$: collapsed = depth < level;
const format = i => {
switch (getType(i)) {
function format(value) {
switch (getType(value)) {
case 'string':
return `${i}`;
return `${value}`;
case 'function':
return 'f () {...}';
case 'symbol':
return i.toString();
return value.toString();
default:
return i;
return value;
}
};
const clicked = e => {
}
function onClick(e) {
if (e.shiftKey) {
if (depth == 0) {
depth = 999;
@ -52,26 +53,9 @@
}
}
collapsed = !collapsed;
};
let invalid = false;
let dbg;
function json2data() {
try {
data = JSON.parse(dbg.value);
invalid = false;
}
catch {
invalid = true;
if (dbg.value.trim == '') {
data = {};
}
}
}
function dragstart(e, keypath, value) {
console.log('kp:', keypath);
function onDragstart(e, keypath, value) {
const item = {};
item[keypath] = value;
e.dataTransfer.setData('text/plain', JSON.stringify(item));
@ -83,22 +67,41 @@
event.stopPropagation();
}
}
function onInput() {
resizeTextarea();
try {
data = JSON.parse(textarea.value);
invalid = false;
}
catch {
invalid = true;
if (textarea.value.trim == '') {
data = {};
}
}
}
function resizeTextarea() {
textarea.style.overflowY = 'hidden';
textarea.style.height = textarea.scrollHeight + 'px';
}
</script>
{#if displayOnly}
{#if items.length}
<span class:root={level == 0} class:hidden={collapsed}>
{#if draggable && isArray}
<span on:dragstart={e => dragstart(e, kp, data)} draggable="true" class="bracket" on:click={clicked} tabindex="0">{openBracket}</span>
<span on:dragstart={e => onDragstart(e, kp, data)} draggable="true" class="bracket" on:click={onClick} tabindex="0">{openBracket}</span>
{:else}
<span class="bracket" on:click={clicked} tabindex="0">{openBracket}</span>
<span class="bracket" on:click={onClick} tabindex="0">{openBracket}</span>
{/if}
<ul on:dblclick={() => (readonly ? displayOnly = true : displayOnly = false)} >
{#each items as i, idx}
<li>
{#if !isArray}
{#if draggable}
<span on:dragstart={e => dragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="key">{i}:</span>
<span on:dragstart={e => onDragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="key">{i}:</span>
{:else}
<span class="key">{i}:</span>
{/if}
@ -107,7 +110,7 @@
<svelte:self {readonly} {draggable} kp={kp ? kp + '.' + i : i} data={data[i]} {depth} level={level + 1} last={idx === items.length - 1} />
{:else}
{#if draggable}
<span on:dragstart={e => dragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="val {getType(data[i])}">{format(data[i])}</span>{#if idx < items.length - 1}<span draggable class="comma">,</span>{/if}
<span on:dragstart={e => onDragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="val {getType(data[i])}">{format(data[i])}</span>{#if idx < items.length - 1}<span draggable class="comma">,</span>{/if}
{:else}
<span class="val {getType(data[i])}">{format(data[i])}</span>{#if idx < items.length - 1}<span class="comma">,</span>{/if}
{/if}
@ -115,16 +118,14 @@
</li>
{/each}
</ul>
<span class="bracket" on:click={clicked} tabindex="0">{closeBracket}</span>{#if !last}<span
class="comma">,</span>
{/if}
<span class="bracket" on:click={onClick} tabindex="0">{closeBracket}</span>{#if !last}<span class="comma">,</span>{/if}
</span>
<span style="padding: {level == 0 ? 10 : 0}px;" class="bracket" class:hidden={!collapsed} on:click={clicked} tabindex="0">{openBracket}{collapsedSymbol}{closeBracket}</span>{#if !last && collapsed}<span class="comma">,</span>{/if}
<span style="padding: {level == 0 ? 10 : 0}px;" class="bracket" class:hidden={!collapsed} on:click={onClick} tabindex="0">{openBracket}{collapsedSymbol}{closeBracket}</span>{#if !last && collapsed}<span class="comma">,</span>{/if}
{:else}
{@html isArray ? '[]' : '{}'}
{/if}
{:else}
<textarea on:keydown="{onKeydown}" class="debug" spellcheck="false" bind:this={dbg} class:invalid on:input={json2data}>{JSON.stringify(data, null, 2)}</textarea>
<textarea on:keydown={onKeydown} spellcheck="false" bind:this={textarea} class:invalid on:input={onInput}>{JSON.stringify(data, null, 2)}</textarea>
{/if}
<style>
@ -187,7 +188,7 @@
cursor: move;
}
textarea.debug {
textarea {
font-family: menlo, monospace;
padding: 10px;
flex: 1 0;
@ -195,12 +196,12 @@
font-size: 90%;
border: none;
margin: 0;
width: 100%;
height: 100%;
outline: none;
line-height: 1.5;
resize: none;
}
textarea.invalid {
background: #ffe3e3;
color: #b30202 !important;

View File

@ -47,6 +47,11 @@
.objectviewer {
position: relative;
}
.objectviewer .code :global(span.root) {
display: block;
}
.buttons {
position: absolute;
top: 0;

View File

@ -20,7 +20,7 @@
<li class:active={tab.key === selectedKey}>
<button class="tab" on:click={() => select(tab.key)}>{tab.title}</button>
{#if tab.closable}
<button class="close" on:click={() => dispatch('closeTab', tab.key)}>
<button class="btn-sm" on:click={() => dispatch('closeTab', tab.key)}>
<Icon name="x" />
</button>
{/if}
@ -86,17 +86,9 @@
cursor: not-allowed;
}
button.close {
.btn-sm {
position: absolute;
right: 7px;
top: 7px;
padding: 3px;
border-radius: 2px;
}
button.close:hover {
background-color: rgba(0, 0, 0, 0.1);
}
button.close:active {
background-color: rgba(0, 0, 0, 0.2);
}
</style>

View File

@ -1,7 +1,7 @@
<script>
import { resolveKeypath, setValue } from '../../../../utils';
import { inputTypes, resolveKeypath, setValue } from '../../../../utils';
import Icon from '../../../../components/icon.svelte';
import FormInput from './forminput.svelte';
import FormInput from '../../../../components/forminput.svelte';
export let item = {};
export let view = {};
@ -12,6 +12,7 @@
const iconMap = {
string: 'text',
objectid: 'anchor',
int: 'hash',
long: 'hash',
uint64: 'hash',
@ -28,14 +29,10 @@
return true;
},
});
function reset(columnKey) {
keypathProxy[columnKey] = undefined;
}
</script>
{#if item && view}
{#each view?.columns?.filter(c => c.inputType !== 'none') || [] as column}
{#each view?.columns?.filter(c => inputTypes.includes(c.inputType)) || [] as column}
<!-- svelte-ignore a11y-label-has-associated-control because FormInput contains one -->
<label class="column">
<div class="label">
@ -49,9 +46,6 @@
</div>
<div class="input">
<FormInput {column} bind:value={keypathProxy[column.key]} bind:valid={validity[column.key]} />
<button type="button" class="btn" title="Reset value" on:click={() => reset(column.key)} disabled={keypathProxy[column.key] === undefined}>
<Icon name="reload" />
</button>
</div>
</label>
{/each}
@ -75,12 +69,6 @@
height: 13px;
}
.input {
display: grid;
grid-template: 1fr / 1fr auto;
gap: 0.5rem;
}
.tag {
display: inline-block;
background-color: rgba(140, 140, 140, 0.1);

View File

@ -1,73 +0,0 @@
<script>
import { isDate } from '../../../../utils';
import { input } from '../../../../actions';
export let column = {};
export let value = undefined;
export let valid = true;
const onValid = () => valid = true;
const onInvalid = () => valid = false;
const numericTypes = [ 'int', 'long', 'uint64', 'double', 'decimal' ];
let dateInput;
let timeInput;
$: type = column.inputType;
$: mandatory = column.mandatory;
$: if (value === undefined) {
dateInput && (dateInput.value = undefined);
timeInput && (timeInput.value = undefined);
mandatory && (valid = false);
}
function setDate(event) {
if (event?.currentTarget?.value) {
if (!isDate(value)) {
value = new Date(event.currentTarget.value);
}
const date = event.currentTarget.value.split('-').map(n => parseInt(n));
value.setFullYear(date[0], date[1], date[2]);
}
}
function setTime(event) {
if (event?.currentTarget?.value) {
const time = event.currentTarget.value.split(':').map(n => parseInt(n));
value.setHours?.(time[0], time[1], 0, 0);
}
}
function selectChange() {
if ((value === undefined) && mandatory) {
valid = false;
}
else {
valid = true;
}
}
</script>
<div class="field {type}">
{#if type === 'string'}
<input type="text" bind:value use:input={{ type, onValid, onInvalid, mandatory }} />
{:else if numericTypes.includes(type)}
<input type="number" bind:value use:input={{ type, onValid, onInvalid, mandatory }} />
{:else if type === 'bool'}
<select bind:value on:change={selectChange}>
<option value={undefined} disabled={mandatory}>Unset</option>
<option value={true}>Yes / true</option>
<option value={false}>No / false</option>
</select>
{:else if type === 'date'}
{@const isNotDate = !isDate(value)}
<input type="date" bind:this={dateInput} on:input={setDate} use:input />
<input type="time" bind:this={timeInput} on:input={setTime} disabled={isNotDate} title={isNotDate ? 'Enter a date first' : ''} />
{/if}
</div>
<style>
.field.date {
display: grid;
grid-template: 1fr / 3fr 1fr;
}
</style>

View File

@ -151,7 +151,7 @@
<option value="none">Hidden in form</option>
<optgroup label="Strings">
<option value="string">String</option>
<option value="objectid">ObjectID</option>
<option value="objectid">ObjectId</option>
</optgroup>
<optgroup label="Integers">
<option value="int">Integer (32-bit, signed)</option>

View File

@ -6,6 +6,8 @@
import Icon from '../../../components/icon.svelte';
import Form from './components/form.svelte';
import ObjectViewer from '../../../components/objectviewer.svelte';
import Grid from '../../../components/grid.svelte';
import { inputTypes, randomString } from '../../../utils';
export let collection;
@ -50,6 +52,10 @@
objectViewerData = [ ...newItems ];
}
}
function addRow() {
newItems = [ ...newItems, {} ];
}
</script>
<form on:submit|preventDefault={insert}>
@ -64,10 +70,29 @@
use:input={{ type: 'json', autofocus: true }}
></textarea>
</label>
{:else}
{:else if viewType === 'form'}
<div class="form">
<Form bind:item={newItems[0]} bind:valid={formValid} view={$views[collection.viewKey]} />
</div>
{:else if viewType === 'table'}
<div class="table">
<Grid
key="id"
items={newItems}
columns={
$views[collection.viewKey]?.columns
?.filter(c => inputTypes.includes(c.inputType))
.map(c => ({ ...c, id: randomString(8), title: c.key })) || []
}
showHeaders={true}
canAddRows={true}
canSelect={false}
canRemoveItems={true}
hideChildrenToggles={true}
on:addRow={addRow}
bind:inputsValid={formValid}
/>
</div>
{/if}
<div class="flex">
@ -114,6 +139,11 @@
gap: 0.5rem;
}
.table {
background-color: #fff;
border: 1px solid #ccc;
}
.flex {
display: flex;
justify-content: space-between;

View File

@ -22,7 +22,7 @@
<label for="autosubmitQuery">Autosubmit query</label>
<span>
<input type="checkbox" id="autosubmitQuery" bind:checked={$settings.autosubmitQuery} />
<label for="autosubmitQuery">Query items automatically when you open a collection</label>
<label for="autosubmitQuery">Query items automatically after opening a collection</label>
</span>
</div>
</Modal>

View File

@ -85,6 +85,22 @@ select:disabled {
cursor: not-allowed;
}
.btn-sm {
padding: 3px;
border-radius: 2px;
}
.btn-sm:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.btn-sm:active {
background-color: rgba(0, 0, 0, 0.2);
}
.btn-sm svg {
width: 13px;
height: 13px;
vertical-align: bottom;
}
.field {
display: flex;
white-space: nowrap;

View File

@ -1,3 +1,4 @@
import { ObjectId } from 'bson';
import { get } from 'svelte/store';
import { environment } from './stores';
@ -11,6 +12,18 @@ export const int32 = [ intMin(32), intMax(32) ];
export const int64 = [ intMin(64), intMax(64) ];
export const uint64 = [ 0, uintMax(64) ];
// Input types
export const numericInputTypes = [ 'int', 'long', 'uint64', 'double', 'decimal' ];
export const inputTypes = [ 'string', 'objectid', 'bool', 'date', ...numericInputTypes ];
// Months
export const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
export const monthsAbbr = months.map(m => m.slice(0, 3));
// Days
export const days = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ];
export const daysAbbr = days.map(d => d.slice(0, 3));
// Get a value from an object with a JSON path, from Webdesq core
export function resolveKeypath(object, path) {
const parts = path.split('.').flatMap(part => {
@ -112,6 +125,12 @@ export function isBsonBuiltin(value) {
);
}
export function isDate(value) {
return (value instanceof Date) && !isNaN(value.getTime());
export function canBeObjectId(value) {
try {
new ObjectId(value);
return true;
}
catch {
return false;
}
}

31
package-lock.json generated Normal file
View File

@ -0,0 +1,31 @@
{
"name": "mongodup",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"date-fns": "^2.29.3"
}
},
"node_modules/date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
}
},
"dependencies": {
"date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"date-fns": "^2.29.3"
}
}