mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-01-18 21:17:59 +00:00
Forms!
This commit is contained in:
parent
2d33c6f2ab
commit
1cf89b3278
@ -1,13 +1,72 @@
|
|||||||
export function input(node, { json, autofocus } = { json: false, autofocus: false }) {
|
import { int32, int64, isInt, uint64 } from './utils';
|
||||||
|
|
||||||
|
export function input(node, { autofocus, type, onValid, onInvalid, mandatory } = {
|
||||||
|
autofocus: false,
|
||||||
|
type: '',
|
||||||
|
onValid: () => 0,
|
||||||
|
onInvalid: () => 0,
|
||||||
|
mandatory: false,
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const getMessage = () => {
|
||||||
|
const checkInteger = () => (isInt(node.value) ? false : 'Value must be an integer');
|
||||||
|
const checkNumberBoundaries = boundaries => {
|
||||||
|
if (node.value < boundaries[0]) {
|
||||||
|
return `Input is too low for type ${type}`;
|
||||||
|
}
|
||||||
|
else if (node.value > boundaries[1]) {
|
||||||
|
return `Input is too high for type ${type}`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'json':
|
||||||
|
try {
|
||||||
|
JSON.parse(node.value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return 'Invalid JSON';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'int': // int32
|
||||||
|
return checkInteger() || checkNumberBoundaries(int32);
|
||||||
|
|
||||||
|
case 'long': // int64
|
||||||
|
return checkInteger() || checkNumberBoundaries(int64);
|
||||||
|
|
||||||
|
case 'uint64':
|
||||||
|
return checkInteger() || checkNumberBoundaries(uint64);
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
if (mandatory && (node.value == '')) {
|
||||||
|
return 'This field cannot empty';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case 'double':
|
||||||
|
case 'decimal':
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleInput = () => {
|
const handleInput = () => {
|
||||||
if (json) {
|
const invalid = getMessage();
|
||||||
try {
|
if (invalid) {
|
||||||
JSON.parse(node.value);
|
node.classList.add('invalid');
|
||||||
node.classList.remove('invalid');
|
node.setCustomValidity(invalid);
|
||||||
}
|
node.reportValidity();
|
||||||
catch {
|
onInvalid?.();
|
||||||
node.classList.add('invalid');
|
}
|
||||||
}
|
else {
|
||||||
|
node.classList.remove('invalid');
|
||||||
|
node.setCustomValidity('');
|
||||||
|
node.reportValidity();
|
||||||
|
onValid?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
{#if item.children?.length}
|
{#if item.children?.length}
|
||||||
<button
|
<button
|
||||||
class="toggle"
|
class="toggle"
|
||||||
on:click={evt => toggleChildren(item[key], evt.shiftKey)}
|
on:click|stopPropagation={evt => toggleChildren(item[key], evt.shiftKey)}
|
||||||
style:transform="translateX({level * 10}px)"
|
style:transform="translateX({level * 10}px)"
|
||||||
>
|
>
|
||||||
<Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} />
|
<Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} />
|
||||||
|
14
frontend/src/components/icon.svelte
vendored
14
frontend/src/components/icon.svelte
vendored
@ -53,11 +53,25 @@
|
|||||||
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>
|
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>
|
||||||
{:else if name === 'table'}
|
{:else if name === 'table'}
|
||||||
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/>
|
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/>
|
||||||
|
{:else if name === 'form'}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/>
|
||||||
{:else if name === 'cog'}
|
{:else if name === 'cog'}
|
||||||
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
{:else if name === 'zap'}
|
{:else if name === 'zap'}
|
||||||
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
|
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||||
{:else if name === 'server'}
|
{:else if name === 'server'}
|
||||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><path d="M6 6h.01M6 18h.01"/>
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><path d="M6 6h.01M6 18h.01"/>
|
||||||
|
{:else if name === 'text'}
|
||||||
|
<path d="M4 7V4h16v3M9 20h6M12 4v16"/>
|
||||||
|
{:else if name === 'hash'}
|
||||||
|
<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/>
|
||||||
|
{:else if name === 'toggle-l'}
|
||||||
|
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/><circle cx="8" cy="12" r="3"/>
|
||||||
|
{:else if name === 'cal'}
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><path d="M16 2v4M8 2v4M3 10h18"/>
|
||||||
|
{:else if name === 'code'}
|
||||||
|
<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"/>
|
||||||
{/if}
|
{/if}
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
<script>
|
||||||
|
import { resolveKeypath, setValue } from '../../../../utils';
|
||||||
|
import Icon from '../../../../components/icon.svelte';
|
||||||
|
import FormInput from './forminput.svelte';
|
||||||
|
|
||||||
|
export let item = {};
|
||||||
|
export let view = {};
|
||||||
|
export let valid = false;
|
||||||
|
|
||||||
|
const validity = {};
|
||||||
|
$: valid = Object.values(validity).every(v => !!v);
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
string: 'text',
|
||||||
|
int: 'hash',
|
||||||
|
long: 'hash',
|
||||||
|
uint64: 'hash',
|
||||||
|
double: 'hash',
|
||||||
|
decimal: 'hash',
|
||||||
|
bool: 'toggle-l',
|
||||||
|
date: 'cal',
|
||||||
|
};
|
||||||
|
|
||||||
|
const keypathProxy = new Proxy(item, {
|
||||||
|
get: (item, key) => resolveKeypath(item, key),
|
||||||
|
set: (item, key, value) => {
|
||||||
|
setValue(item, key, value);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function reset(columnKey) {
|
||||||
|
keypathProxy[columnKey] = undefined;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if item && view}
|
||||||
|
{#each view?.columns?.filter(c => c.inputType !== 'none') || [] as column}
|
||||||
|
<!-- svelte-ignore a11y-label-has-associated-control because FormInput contains one -->
|
||||||
|
<label class="column">
|
||||||
|
<div class="label">
|
||||||
|
<Icon name={iconMap[column.inputType]} />
|
||||||
|
<span>
|
||||||
|
{column.key}
|
||||||
|
{#if column.mandatory}
|
||||||
|
<span class="tag" class:invalid={!validity[column.key]}>mandatory</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</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]}>
|
||||||
|
<Icon name="reload" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.column {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.column + .column {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.column .label {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.column .label :global(svg) {
|
||||||
|
width: 13px;
|
||||||
|
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);
|
||||||
|
color: #777;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tag.invalid {
|
||||||
|
background-color: rgba(255, 80, 80, 0.3);
|
||||||
|
color: #8d2c2c;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,76 @@
|
|||||||
|
<script>
|
||||||
|
import { isDate } from '../../../../utils';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { input } from '../../../../actions';
|
||||||
|
|
||||||
|
export let column = {};
|
||||||
|
export let value = undefined;
|
||||||
|
export let valid = true;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
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;
|
||||||
|
$: dispatch('input', value);
|
||||||
|
|
||||||
|
$: 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>
|
@ -1,8 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import Icon from '../../../components/icon.svelte';
|
import Icon from '../../../../components/icon.svelte';
|
||||||
import { input } from '../../../actions';
|
import { input } from '../../../../actions';
|
||||||
import Modal from '../../../components/modal.svelte';
|
import Modal from '../../../../components/modal.svelte';
|
||||||
import { CreateIndex } from '../../../../wailsjs/go/app/App';
|
import { CreateIndex } from '../../../../../wailsjs/go/app/App';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
export let collection = {};
|
export let collection = {};
|
@ -0,0 +1,242 @@
|
|||||||
|
<script>
|
||||||
|
import TabBar from '../../../../components/tabbar.svelte';
|
||||||
|
import Modal from '../../../../components/modal.svelte';
|
||||||
|
import Icon from '../../../../components/icon.svelte';
|
||||||
|
import { views } from '../../../../stores';
|
||||||
|
import { randomString } from '../../../../utils';
|
||||||
|
import { input } from '../../../../actions';
|
||||||
|
|
||||||
|
export let collection;
|
||||||
|
export let show = false;
|
||||||
|
export let activeViewKey = 'list';
|
||||||
|
export let firstItem = {};
|
||||||
|
|
||||||
|
$: tabs = Object.entries(views.forCollection(collection.hostKey, collection.dbKey, collection.key))
|
||||||
|
.sort((a, b) => sortTabKeys(a[0], b[0]))
|
||||||
|
.map(([ key, v ]) => ({ key, title: v.name, closable: key !== 'list' }));
|
||||||
|
|
||||||
|
function sortTabKeys(a, b) {
|
||||||
|
if (a === 'list') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b === 'list') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return a.localeCompare(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createView() {
|
||||||
|
const newViewKey = randomString();
|
||||||
|
$views[newViewKey] = {
|
||||||
|
name: 'Table view',
|
||||||
|
host: collection.hostKey,
|
||||||
|
database: collection.dbKey,
|
||||||
|
collection: collection.key,
|
||||||
|
type: 'table',
|
||||||
|
columns: [ { key: '_id', showInTable: true, inputType: 'string' } ],
|
||||||
|
};
|
||||||
|
activeViewKey = newViewKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeView(viewKey) {
|
||||||
|
const keys = Object.keys($views).sort(sortTabKeys);
|
||||||
|
const oldIndex = keys.indexOf(viewKey);
|
||||||
|
const newKey = keys[oldIndex - 1];
|
||||||
|
activeViewKey = newKey;
|
||||||
|
delete $views[viewKey];
|
||||||
|
$views = $views;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addColumn(before) {
|
||||||
|
if (typeof before === 'number') {
|
||||||
|
$views[activeViewKey].columns = [
|
||||||
|
...$views[activeViewKey].columns.slice(0, before),
|
||||||
|
{ showInTable: true, inputType: 'none' },
|
||||||
|
...$views[activeViewKey].columns.slice(before),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$views[activeViewKey].columns = [
|
||||||
|
...$views[activeViewKey].columns,
|
||||||
|
{ showInTable: true, inputType: 'none' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSuggestedColumns() {
|
||||||
|
if ((typeof firstItem !== 'object') || (firstItem === null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$views[activeViewKey].columns = Object.keys(firstItem).sort().map(key => ({
|
||||||
|
key,
|
||||||
|
showInTable: true,
|
||||||
|
inputType: 'none',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveColumn(oldIndex, delta) {
|
||||||
|
const column = $views[activeViewKey].columns[oldIndex];
|
||||||
|
const newIndex = oldIndex + delta;
|
||||||
|
|
||||||
|
$views[activeViewKey].columns.splice(oldIndex, 1);
|
||||||
|
$views[activeViewKey].columns.splice(newIndex, 0, column);
|
||||||
|
$views[activeViewKey].columns = $views[activeViewKey].columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeColumn(index) {
|
||||||
|
$views[activeViewKey].columns.splice(index, 1);
|
||||||
|
$views[activeViewKey].columns = $views[activeViewKey].columns;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal title="View configuration" bind:show contentPadding={false}>
|
||||||
|
<TabBar
|
||||||
|
{tabs}
|
||||||
|
canAddTab={true}
|
||||||
|
on:addTab={createView}
|
||||||
|
on:closeTab={e => removeView(e.detail)}
|
||||||
|
bind:selectedKey={activeViewKey}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
{#if $views[activeViewKey]}
|
||||||
|
<div class="meta">
|
||||||
|
{#key activeViewKey}
|
||||||
|
<label class="field">
|
||||||
|
<span class="label">View name</span>
|
||||||
|
<input type="text" use:input={{ autofocus: true }} bind:value={$views[activeViewKey].name} disabled={activeViewKey === 'list'} />
|
||||||
|
</label>
|
||||||
|
{/key}
|
||||||
|
<label class="field">
|
||||||
|
<span class="label">View type</span>
|
||||||
|
<select bind:value={$views[activeViewKey].type} disabled>
|
||||||
|
<option value="list">List view</option>
|
||||||
|
<option value="table">Table view</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $views[activeViewKey].type === 'list'}
|
||||||
|
<div class="flex">
|
||||||
|
<input type="checkbox" id="hideObjectIndicators" bind:checked={$views[activeViewKey].hideObjectIndicators} />
|
||||||
|
<label for="hideObjectIndicators">
|
||||||
|
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{:else if $views[activeViewKey].type === 'table'}
|
||||||
|
<div class="columns">
|
||||||
|
{#each $views[activeViewKey].columns as column, columnIndex}
|
||||||
|
<div class="column">
|
||||||
|
<label class="field">
|
||||||
|
<input type="text" use:input bind:value={column.key} placeholder="Column keypath" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field" title="Show column in table view">
|
||||||
|
<span class="label">
|
||||||
|
<Icon name="table" />
|
||||||
|
</span>
|
||||||
|
<span class="checkbox">
|
||||||
|
<input type="checkbox" bind:checked={column.showInTable} />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field" title="Input type in form view">
|
||||||
|
<span class="label">
|
||||||
|
<Icon name="form" />
|
||||||
|
</span>
|
||||||
|
<select bind:value={column.inputType}>
|
||||||
|
<option value="none">Hidden in form</option>
|
||||||
|
<optgroup label="Strings">
|
||||||
|
<option value="string">String</option>
|
||||||
|
<option value="objectid">ObjectID</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Integers">
|
||||||
|
<option value="int">Integer (32-bit, signed)</option>
|
||||||
|
<option value="uint64">Integer (64-bit, unsigned)</option>
|
||||||
|
<option value="long">Long (64-bit integer, signed)</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Floats">
|
||||||
|
<option value="double">Double (64-bit)</option>
|
||||||
|
<option value="decimal">Decimal (128-bit)</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Miscellaneous">
|
||||||
|
<option value="bool">Boolean</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field" title="Mandatory (field must be valid in order to submit form)">
|
||||||
|
<span class="label">
|
||||||
|
<Icon name="target" />
|
||||||
|
</span>
|
||||||
|
<span class="checkbox">
|
||||||
|
<input type="checkbox" bind:checked={column.mandatory} disabled={column.inputType === 'none'} />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="btn" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one">
|
||||||
|
<Icon name="+" />
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up">
|
||||||
|
<Icon name="chev-u" />
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, 1)} disabled={columnIndex === $views[activeViewKey].columns.length - 1} title="Move column one position down">
|
||||||
|
<Icon name="chev-d" />
|
||||||
|
</button>
|
||||||
|
<button class="btn danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column">
|
||||||
|
<Icon name="x" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p>No columns yet</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button class="btn" on:click={addColumn}>
|
||||||
|
<Icon name="+" /> Add column
|
||||||
|
</button>
|
||||||
|
<button class="btn" on:click={addSuggestedColumns} disabled={!Object.keys(firstItem || {}).length}>
|
||||||
|
<Icon name="zap" /> Add suggested columns
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.options {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template: 1fr / 1fr 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.5rem 0.5rem 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.columns p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.columns .column {
|
||||||
|
display: grid;
|
||||||
|
grid-template: 1fr / 1fr repeat(7, auto);
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,182 +0,0 @@
|
|||||||
<script>
|
|
||||||
import TabBar from '../../../components/tabbar.svelte';
|
|
||||||
import Modal from '../../../components/modal.svelte';
|
|
||||||
import Icon from '../../../components/icon.svelte';
|
|
||||||
import { views } from '../../../stores';
|
|
||||||
import { randomString } from '../../../utils';
|
|
||||||
import { input } from '../../../actions';
|
|
||||||
|
|
||||||
export let collection;
|
|
||||||
export let show = false;
|
|
||||||
export let activeViewKey = 'list';
|
|
||||||
export let firstItem = {};
|
|
||||||
|
|
||||||
$: tabs = Object.entries($views).filter(v => (
|
|
||||||
v[0] === 'list' || (
|
|
||||||
v[1].host === collection.hostKey &&
|
|
||||||
v[1].database === collection.dbKey &&
|
|
||||||
v[1].collection === collection.key
|
|
||||||
)
|
|
||||||
)).sort((a, b) => sortTabKeys(a[0], b[0]))
|
|
||||||
.map(([ key, v ]) => ({ key, title: v.name, closable: key !== 'list' }));
|
|
||||||
|
|
||||||
function sortTabKeys(a, b) {
|
|
||||||
if (a === 'list') {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (b === 'list') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return a.localeCompare(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createView() {
|
|
||||||
const newViewKey = randomString();
|
|
||||||
$views[newViewKey] = {
|
|
||||||
name: 'Table view',
|
|
||||||
host: collection.hostKey,
|
|
||||||
database: collection.dbKey,
|
|
||||||
collection: collection.key,
|
|
||||||
type: 'table',
|
|
||||||
columns: [ { key: '_id' } ],
|
|
||||||
};
|
|
||||||
activeViewKey = newViewKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeView(viewKey) {
|
|
||||||
const keys = Object.keys($views).sort(sortTabKeys);
|
|
||||||
const oldIndex = keys.indexOf(viewKey);
|
|
||||||
const newKey = keys[oldIndex - 1];
|
|
||||||
console.log(keys, oldIndex, newKey);
|
|
||||||
activeViewKey = newKey;
|
|
||||||
delete $views[viewKey];
|
|
||||||
$views = $views;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addColumn(before) {
|
|
||||||
if (typeof before === 'number') {
|
|
||||||
$views[activeViewKey].columns = [
|
|
||||||
...$views[activeViewKey].columns.slice(0, before),
|
|
||||||
{},
|
|
||||||
...$views[activeViewKey].columns.slice(before),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$views[activeViewKey].columns = [ ...$views[activeViewKey].columns, {} ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSuggestedColumns() {
|
|
||||||
if ((typeof firstItem !== 'object') || (firstItem === null)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$views[activeViewKey].columns = Object.keys(firstItem).map(key => ({ key }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveColumn(oldIndex, delta) {
|
|
||||||
const column = $views[activeViewKey].columns[oldIndex];
|
|
||||||
const newIndex = oldIndex + delta;
|
|
||||||
|
|
||||||
$views[activeViewKey].columns.splice(oldIndex, 1);
|
|
||||||
$views[activeViewKey].columns.splice(newIndex, 0, column);
|
|
||||||
$views[activeViewKey].columns = $views[activeViewKey].columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeColumn(index) {
|
|
||||||
$views[activeViewKey].columns.splice(index, 1);
|
|
||||||
$views[activeViewKey].columns = $views[activeViewKey].columns;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal title="View configuration" bind:show contentPadding={false}>
|
|
||||||
<TabBar
|
|
||||||
{tabs}
|
|
||||||
canAddTab={true}
|
|
||||||
on:addTab={createView}
|
|
||||||
on:closeTab={e => removeView(e.detail)}
|
|
||||||
bind:selectedKey={activeViewKey}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="options">
|
|
||||||
{#if $views[activeViewKey]}
|
|
||||||
<div class="meta">
|
|
||||||
{#key activeViewKey}
|
|
||||||
<label class="field">
|
|
||||||
<span class="label">View name</span>
|
|
||||||
<input type="text" use:input={{ autofocus: true }} bind:value={$views[activeViewKey].name} disabled={activeViewKey === 'list'} />
|
|
||||||
</label>
|
|
||||||
{/key}
|
|
||||||
<label class="field">
|
|
||||||
<span class="label">View type</span>
|
|
||||||
<select bind:value={$views[activeViewKey].type} disabled={activeViewKey === 'list'}>
|
|
||||||
<option value="list">List view</option>
|
|
||||||
<option value="table">Table view</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $views[activeViewKey].type === 'list'}
|
|
||||||
<div class="flex">
|
|
||||||
<input type="checkbox" id="hideObjectIndicators" bind:checked={$views[activeViewKey].hideObjectIndicators} />
|
|
||||||
<label for="hideObjectIndicators">
|
|
||||||
Hide object indicators ({'{...}'} and [...]) in list view and show nothing instead
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{:else if $views[activeViewKey].type === 'table'}
|
|
||||||
{#each $views[activeViewKey].columns as column, columnIndex}
|
|
||||||
<div class="column">
|
|
||||||
<label class="field">
|
|
||||||
<input type="text" use:input bind:value={column.key} placeholder="Column keypath" />
|
|
||||||
</label>
|
|
||||||
<button class="btn" type="button" on:click={() => addColumn(columnIndex)} title="Add column before this one">
|
|
||||||
<Icon name="+" />
|
|
||||||
</button>
|
|
||||||
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, -1)} disabled={columnIndex === 0} title="Move column one position up">
|
|
||||||
<Icon name="chev-u" />
|
|
||||||
</button>
|
|
||||||
<button class="btn" type="button" on:click={() => moveColumn(columnIndex, 1)} disabled={columnIndex === $views[activeViewKey].columns.length - 1} title="Move column one position down">
|
|
||||||
<Icon name="chev-d" />
|
|
||||||
</button>
|
|
||||||
<button class="btn danger" type="button" on:click={() => removeColumn(columnIndex)} title="Remove this column">
|
|
||||||
<Icon name="x" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<button class="btn" on:click={addColumn}>
|
|
||||||
<Icon name="+" /> Add column
|
|
||||||
</button>
|
|
||||||
<button class="btn" on:click={addSuggestedColumns} disabled={!firstItem}>
|
|
||||||
<Icon name="zap" /> Add suggested columns
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.options {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
display: grid;
|
|
||||||
grid-template: 1fr / 1fr 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
display: grid;
|
|
||||||
grid-template: 1fr / 1fr repeat(4, auto);
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,18 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import { FindItems, RemoveItemById } from '../../../../wailsjs/go/app/App';
|
|
||||||
import CodeExample from '../../../components/code-example.svelte';
|
|
||||||
import { input } from '../../../actions';
|
|
||||||
import ObjectGrid from '../../../components/objectgrid.svelte';
|
|
||||||
import Icon from '../../../components/icon.svelte';
|
|
||||||
import ObjectViewer from '../../../components/objectviewer.svelte';
|
|
||||||
import FindViewConfigModal from './find-viewconfig.svelte';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import Grid from '../../../components/grid.svelte';
|
|
||||||
import { applicationSettings, views } from '../../../stores';
|
|
||||||
import { EJSON } from 'bson';
|
import { EJSON } from 'bson';
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { FindItems, RemoveItemById } from '../../../../wailsjs/go/app/App';
|
||||||
|
import { input } from '../../../actions';
|
||||||
|
import CodeExample from '../../../components/code-example.svelte';
|
||||||
|
import Grid from '../../../components/grid.svelte';
|
||||||
|
import Icon from '../../../components/icon.svelte';
|
||||||
|
import ObjectGrid from '../../../components/objectgrid.svelte';
|
||||||
|
import ObjectViewer from '../../../components/objectviewer.svelte';
|
||||||
|
import { applicationSettings, views } from '../../../stores';
|
||||||
|
|
||||||
export let collection;
|
export let collection;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
const defaults = {
|
const defaults = {
|
||||||
query: '{}',
|
query: '{}',
|
||||||
sort: $applicationSettings.defaultSort || '{ "_id": 1 }',
|
sort: $applicationSettings.defaultSort || '{ "_id": 1 }',
|
||||||
@ -22,19 +22,16 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let form = { ...defaults };
|
let form = { ...defaults };
|
||||||
let activeViewKey = 'list';
|
|
||||||
let result = {};
|
let result = {};
|
||||||
let submittedForm = {};
|
let submittedForm = {};
|
||||||
let queryField;
|
let queryField;
|
||||||
let activePath = [];
|
let activePath = [];
|
||||||
let objectViewerData;
|
let objectViewerData;
|
||||||
let viewConfigModalOpen = false;
|
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
|
||||||
$: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
|
$: code = `db.${collection.key}.find(${form.query || '{}'}${form.fields && form.fields !== '{}' ? `, ${form.fields}` : ''}).sort(${form.sort})${form.skip ? `.skip(${form.skip})` : ''}${form.limit ? `.limit(${form.limit})` : ''};`;
|
||||||
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
|
$: lastPage = (submittedForm.limit && result?.results?.length) ? Math.max(0, Math.ceil((result.total - submittedForm.limit) / submittedForm.limit)) : 0;
|
||||||
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
|
$: activePage = (submittedForm.limit && submittedForm.skip && result?.results?.length) ? submittedForm.skip / submittedForm.limit : 0;
|
||||||
|
|
||||||
$: collection && refresh();
|
|
||||||
|
|
||||||
async function submitQuery() {
|
async function submitQuery() {
|
||||||
activePath = [];
|
activePath = [];
|
||||||
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form));
|
const newResult = await FindItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(form));
|
||||||
@ -100,6 +97,7 @@
|
|||||||
submitQuery();
|
submitQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: collection && refresh();
|
||||||
onMount(refresh);
|
onMount(refresh);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -108,19 +106,19 @@
|
|||||||
<div class="form-row one">
|
<div class="form-row one">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="label">Query or id</span>
|
<span class="label">Query or id</span>
|
||||||
<input type="text" class="code" bind:this={queryField} bind:value={form.query} use:input={{ json: true, autofocus: true }} placeholder={defaults.query} />
|
<input type="text" class="code" bind:this={queryField} bind:value={form.query} use:input={{ type: 'json', autofocus: true }} placeholder={defaults.query} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="label">Sort</span>
|
<span class="label">Sort</span>
|
||||||
<input type="text" class="code" bind:value={form.sort} use:input={{ json: true }} placeholder={defaults.sort} />
|
<input type="text" class="code" bind:value={form.sort} use:input={{ type: 'json' }} placeholder={defaults.sort} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row two">
|
<div class="form-row two">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="label">Fields</span>
|
<span class="label">Fields</span>
|
||||||
<input type="text" class="code" bind:value={form.fields} use:input={{ json: true }} placeholder={defaults.fields} />
|
<input type="text" class="code" bind:value={form.fields} use:input={{ type: 'json' }} placeholder={defaults.fields} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@ -142,19 +140,19 @@
|
|||||||
<div class="result">
|
<div class="result">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#key result}
|
{#key result}
|
||||||
{#if activeViewKey === 'table'}
|
{#if collection.viewKey === 'list'}
|
||||||
<Grid
|
<ObjectGrid
|
||||||
key="_id"
|
data={result.results}
|
||||||
columns={$views[activeViewKey]?.columns?.map(c => ({ key: c.key, title: c.key })) || []}
|
hideObjectIndicators={$views[collection.viewKey]?.hideObjectIndicators}
|
||||||
showHeaders={true}
|
|
||||||
items={result.results || []}
|
|
||||||
bind:activePath
|
bind:activePath
|
||||||
on:trigger={e => openJson(e.detail?.itemKey)}
|
on:trigger={e => openJson(e.detail?.itemKey)}
|
||||||
/>
|
/>
|
||||||
{:else if activeViewKey === 'list'}
|
{:else}
|
||||||
<ObjectGrid
|
<Grid
|
||||||
data={result.results}
|
key="_id"
|
||||||
hideObjectIndicators={$views[activeViewKey]?.hideObjectIndicators}
|
columns={$views[collection.viewKey]?.columns?.map(c => ({ key: c.key, title: c.key })) || []}
|
||||||
|
showHeaders={true}
|
||||||
|
items={result.results ? result.results.map(r => EJSON.deserialize(r)) : []}
|
||||||
bind:activePath
|
bind:activePath
|
||||||
on:trigger={e => openJson(e.detail?.itemKey)}
|
on:trigger={e => openJson(e.detail?.itemKey)}
|
||||||
/>
|
/>
|
||||||
@ -169,9 +167,16 @@
|
|||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn" on:click={() => viewConfigModalOpen = true} title="Configure view">
|
<label class="field inline">
|
||||||
<Icon name="cog" />
|
<select bind:value={collection.viewKey}>
|
||||||
</button>
|
{#each Object.entries(viewsForCollection) as [key, view]}
|
||||||
|
<option value={key}>{view.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button class="btn" on:click={() => dispatch('openViewConfig', { firstItem: result.results?.[0] })} title="Configure view">
|
||||||
|
<Icon name="cog" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
<button class="btn danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item">
|
<button class="btn danger" on:click={removeActive} disabled={!activePath?.length} title="Drop selected item">
|
||||||
<Icon name="-" />
|
<Icon name="-" />
|
||||||
</button>
|
</button>
|
||||||
@ -193,12 +198,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ObjectViewer bind:data={objectViewerData} />
|
<ObjectViewer bind:data={objectViewerData} />
|
||||||
<FindViewConfigModal
|
|
||||||
bind:show={viewConfigModalOpen}
|
|
||||||
bind:activeViewKey
|
|
||||||
firstItem={result.results?.[0]}
|
|
||||||
{collection}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<datalist id="limits">
|
<datalist id="limits">
|
||||||
{#each [ 1, 5, 10, 25, 50, 100, 200 ] as value}
|
{#each [ 1, 5, 10, 25, 50, 100, 200 ] as value}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import Stats from './stats.svelte';
|
import Stats from './stats.svelte';
|
||||||
import Update from './update.svelte';
|
import Update from './update.svelte';
|
||||||
import { EventsOn } from '../../../../wailsjs/runtime/runtime';
|
import { EventsOn } from '../../../../wailsjs/runtime/runtime';
|
||||||
|
import ViewConfig from './components/viewconfig.svelte';
|
||||||
|
|
||||||
export let collection;
|
export let collection;
|
||||||
export let hostKey;
|
export let hostKey;
|
||||||
@ -17,6 +18,8 @@
|
|||||||
|
|
||||||
let tab = 'find';
|
let tab = 'find';
|
||||||
let find;
|
let find;
|
||||||
|
let viewConfigModalOpen = false;
|
||||||
|
let firstItem;
|
||||||
|
|
||||||
$: if (collection) {
|
$: if (collection) {
|
||||||
collection.hostKey = hostKey;
|
collection.hostKey = hostKey;
|
||||||
@ -31,6 +34,11 @@
|
|||||||
await tick();
|
await tick();
|
||||||
find.performQuery(event.detail);
|
find.performQuery(event.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openViewConfig(event) {
|
||||||
|
firstItem = event.detail?.firstItem;
|
||||||
|
viewConfigModalOpen = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="collection" class:empty={!collection}>
|
<div class="collection" class:empty={!collection}>
|
||||||
@ -47,8 +55,8 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if tab === 'stats'} <Stats {collection} />
|
{#if tab === 'stats'} <Stats {collection} />
|
||||||
{:else if tab === 'find'} <Find {collection} bind:this={find} />
|
{:else if tab === 'find'} <Find {collection} bind:this={find} on:openViewConfig={openViewConfig} />
|
||||||
{:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} />
|
{:else if tab === 'insert'} <Insert {collection} on:performFind={catchQuery} on:openViewConfig={openViewConfig} />
|
||||||
{:else if tab === 'update'} <Update {collection} on:performFind={catchQuery} />
|
{:else if tab === 'update'} <Update {collection} on:performFind={catchQuery} />
|
||||||
{:else if tab === 'remove'} <Remove {collection} />
|
{:else if tab === 'remove'} <Remove {collection} />
|
||||||
{:else if tab === 'indexes'} <Indexes {collection} />
|
{:else if tab === 'indexes'} <Indexes {collection} />
|
||||||
@ -60,6 +68,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if collection}
|
||||||
|
<ViewConfig
|
||||||
|
bind:show={viewConfigModalOpen}
|
||||||
|
bind:activeViewKey={collection.viewKey}
|
||||||
|
{firstItem}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.collection {
|
.collection {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import ObjectGrid from '../../../components/objectgrid.svelte';
|
import ObjectGrid from '../../../components/objectgrid.svelte';
|
||||||
import { DropIndex, GetIndexes } from '../../../../wailsjs/go/app/App';
|
import { DropIndex, GetIndexes } from '../../../../wailsjs/go/app/App';
|
||||||
import Icon from '../../../components/icon.svelte';
|
import Icon from '../../../components/icon.svelte';
|
||||||
import IndexDetail from './indexes-detail.svelte';
|
import IndexDetail from './components/indexdetail.svelte';
|
||||||
|
|
||||||
export let collection;
|
export let collection;
|
||||||
|
|
||||||
|
@ -1,17 +1,34 @@
|
|||||||
<script>
|
<script>
|
||||||
import { input } from '../../../actions';
|
import { views } from '../../../stores';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { InsertItems } from '../../../../wailsjs/go/app/App';
|
import { InsertItems } from '../../../../wailsjs/go/app/App';
|
||||||
|
import { input } from '../../../actions';
|
||||||
import Icon from '../../../components/icon.svelte';
|
import Icon from '../../../components/icon.svelte';
|
||||||
|
import Form from './components/form.svelte';
|
||||||
|
import ObjectViewer from '../../../components/objectviewer.svelte';
|
||||||
|
|
||||||
export let collection;
|
export let collection;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let json = '';
|
let json = '';
|
||||||
|
let newItems = [];
|
||||||
let insertedIds;
|
let insertedIds;
|
||||||
|
let objectViewerData = '';
|
||||||
|
let viewType = 'form';
|
||||||
|
let formValid = false;
|
||||||
|
$: viewsForCollection = views.forCollection(collection.hostKey, collection.dbKey, collection.key);
|
||||||
|
$: oppositeViewType = viewType === 'table' ? 'form' : 'table';
|
||||||
|
|
||||||
async function insert() {
|
async function insert() {
|
||||||
insertedIds = await InsertItems(collection.hostKey, collection.dbKey, collection.key, json);
|
if (collection.viewKey === 'list') {
|
||||||
|
insertedIds = await InsertItems(collection.hostKey, collection.dbKey, collection.key, json);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
insertedIds = await InsertItems(collection.hostKey, collection.dbKey, collection.key, JSON.stringify(newItems));
|
||||||
|
if (insertedIds) {
|
||||||
|
newItems = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDocs() {
|
function showDocs() {
|
||||||
@ -21,19 +38,38 @@
|
|||||||
: `{ "_id": { "$in": [ ${insertedIds.map(id => JSON.stringify(id)).join(', ')} ] } }`,
|
: `{ "_id": { "$in": [ ${insertedIds.map(id => JSON.stringify(id)).join(', ')} ] } }`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchViewType() {
|
||||||
|
viewType = oppositeViewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showJson() {
|
||||||
|
if (viewType === 'form') {
|
||||||
|
objectViewerData = { ...(newItems[0] || {}) };
|
||||||
|
}
|
||||||
|
else if (viewType === 'table') {
|
||||||
|
objectViewerData = [ ...newItems ];
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form on:submit|preventDefault={insert}>
|
<form on:submit|preventDefault={insert}>
|
||||||
<label class="field">
|
{#if collection.viewKey === 'list'}
|
||||||
<textarea
|
<label class="field">
|
||||||
cols="30"
|
<textarea
|
||||||
rows="10"
|
cols="30"
|
||||||
placeholder="[]"
|
rows="10"
|
||||||
class="code"
|
placeholder="[]"
|
||||||
bind:value={json}
|
class="code"
|
||||||
use:input={{ json: true, autofocus: true }}
|
bind:value={json}
|
||||||
></textarea>
|
use:input={{ type: 'json', autofocus: true }}
|
||||||
</label>
|
></textarea>
|
||||||
|
</label>
|
||||||
|
{:else}
|
||||||
|
<div class="form">
|
||||||
|
<Form bind:item={newItems[0]} bind:valid={formValid} view={$views[collection.viewKey]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div>
|
<div>
|
||||||
@ -45,13 +81,33 @@
|
|||||||
{#if insertedIds}
|
{#if insertedIds}
|
||||||
<button class="btn" type="button" on:click={showDocs}>View inserted docs</button>
|
<button class="btn" type="button" on:click={showDocs}>View inserted docs</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button type="submit" class="btn" disabled={!json}>
|
{#if collection.viewKey !== 'list'}
|
||||||
|
<button class="btn" type="button" on:click={showJson} title="Show JSON">
|
||||||
|
<Icon name="code" />
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" on:click={switchViewType} title="Edit as {oppositeViewType}">
|
||||||
|
<Icon name={oppositeViewType} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<label class="field inline">
|
||||||
|
<select bind:value={collection.viewKey}>
|
||||||
|
{#each Object.entries(viewsForCollection) as [key, view]}
|
||||||
|
<option value={key}>{key === 'list' ? 'Raw JSON' : view.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button class="btn" type="button" on:click={() => dispatch('openViewConfig')} title="Configure view">
|
||||||
|
<Icon name="cog" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="btn" disabled={$views[collection.viewKey]?.type === 'list' ? !json : !formValid}>
|
||||||
<Icon name="+" /> Insert
|
<Icon name="+" /> Insert
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<ObjectViewer data={objectViewerData} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
form {
|
form {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
placeholder={'{}'}
|
placeholder={'{}'}
|
||||||
class="code"
|
class="code"
|
||||||
bind:value={json}
|
bind:value={json}
|
||||||
use:input={{ json: true, autofocus: true }}
|
use:input={{ type: 'json', autofocus: true }}
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -124,14 +124,14 @@
|
|||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="label">Filter</span>
|
<span class="label">Filter</span>
|
||||||
<input type="text" class="code" bind:value={form.query} use:input={{ json: true, autofocus: true }} placeholder={'{}'} />
|
<input type="text" class="code" bind:value={form.query} use:input={{ type: 'json', autofocus: true }} placeholder={'{}'} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<fieldset class="parameters">
|
<fieldset class="parameters">
|
||||||
{#each form.parameters as param, index}
|
{#each form.parameters as param, index}
|
||||||
<fieldset class="parameter">
|
<fieldset class="parameter">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<select bind:value={param.type}>
|
<select bind:value={param.type} class="type">
|
||||||
{#each Object.entries(atomicUpdateOperators) as [groupName, options]}
|
{#each Object.entries(atomicUpdateOperators) as [groupName, options]}
|
||||||
<optgroup label={groupName}>
|
<optgroup label={groupName}>
|
||||||
{#each Object.entries(options) as [key, label]}
|
{#each Object.entries(options) as [key, label]}
|
||||||
@ -142,18 +142,17 @@
|
|||||||
</optgroup>
|
</optgroup>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
<input type="text" class="code" bind:value={param.value} placeholder={'{}'} use:input={{ type: 'json' }} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<input type="text" class="code" bind:value={param.value} placeholder={'{}'} use:input={{ json: true }} />
|
<button class="btn" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button">
|
||||||
|
<Icon name="+" />
|
||||||
|
</button>
|
||||||
|
<button class="btn" disabled={form.parameters.length < 2} on:click={() => removeParam(index)} type="button">
|
||||||
|
<Icon name="-" />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button class="btn" disabled={form.parameters.length >= allOperators.length} on:click={() => addParameter()} type="button">
|
|
||||||
<Icon name="+" />
|
|
||||||
</button>
|
|
||||||
<button class="btn" disabled={form.parameters.length < 2} on:click={() => removeParam(index)} type="button">
|
|
||||||
<Icon name="-" />
|
|
||||||
</button>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{/each}
|
{/each}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -182,7 +181,11 @@
|
|||||||
}
|
}
|
||||||
.parameter {
|
.parameter {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template: 1fr / auto 1fr auto auto;
|
grid-template: 1fr / 1fr auto;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select.type {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -7,8 +7,6 @@
|
|||||||
export let info;
|
export let info;
|
||||||
export let hosts = {};
|
export let hosts = {};
|
||||||
|
|
||||||
$: console.log(info);
|
|
||||||
|
|
||||||
async function selectHost(hostKey) {
|
async function selectHost(hostKey) {
|
||||||
info.hostKey = hostKey;
|
info.hostKey = hostKey;
|
||||||
info.dbKey = undefined;
|
info.dbKey = undefined;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<label for="defaultSort">Default sort query</label>
|
<label for="defaultSort">Default sort query</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<input type="text" class="code" bind:value={$settings.defaultSort} id="defaultSort" use:input={{ json: true }} />
|
<input type="text" class="code" bind:value={$settings.defaultSort} id="defaultSort" use:input={{ type: 'json' }} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label for="autosubmitQuery">Autosubmit query</label>
|
<label for="autosubmitQuery">Autosubmit query</label>
|
||||||
|
@ -43,8 +43,14 @@ export const applicationSettings = (() => {
|
|||||||
return newSettings;
|
return newSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let skipUpdate = true;
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
subscribe(newSettings => {
|
subscribe(newSettings => {
|
||||||
|
if (skipUpdate) {
|
||||||
|
skipUpdate = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
UpdateSettings(JSON.stringify(newSettings || {}));
|
UpdateSettings(JSON.stringify(newSettings || {}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,7 +62,6 @@ export const environment = (() => {
|
|||||||
const reload = async() => {
|
const reload = async() => {
|
||||||
const newEnv = await Environment();
|
const newEnv = await Environment();
|
||||||
set(newEnv);
|
set(newEnv);
|
||||||
console.log(newEnv);
|
|
||||||
return newEnv;
|
return newEnv;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,16 +71,38 @@ export const environment = (() => {
|
|||||||
|
|
||||||
export const views = (() => {
|
export const views = (() => {
|
||||||
const { set, subscribe } = writable({});
|
const { set, subscribe } = writable({});
|
||||||
|
|
||||||
const reload = async() => {
|
const reload = async() => {
|
||||||
const newViewStore = await Views();
|
const newViewStore = await Views();
|
||||||
set(newViewStore);
|
set(newViewStore);
|
||||||
return newViewStore;
|
return newViewStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const forCollection = (hostKey, dbKey, collKey) => {
|
||||||
|
let allViews;
|
||||||
|
subscribe(v => allViews = v)();
|
||||||
|
|
||||||
|
const entries = Object.entries(allViews).filter(v => (
|
||||||
|
v[0] === 'list' || (
|
||||||
|
v[1].host === hostKey &&
|
||||||
|
v[1].database === dbKey &&
|
||||||
|
v[1].collection === collKey
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
return Object.fromEntries(entries);
|
||||||
|
};
|
||||||
|
|
||||||
|
let skipUpdate = true;
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
subscribe(newViewStore => {
|
subscribe(newViewStore => {
|
||||||
|
if (skipUpdate) {
|
||||||
|
skipUpdate = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
UpdateViewStore(JSON.stringify(newViewStore));
|
UpdateViewStore(JSON.stringify(newViewStore));
|
||||||
});
|
});
|
||||||
|
|
||||||
return { reload, set, subscribe };
|
return { reload, set, subscribe, forCollection };
|
||||||
})();
|
})();
|
||||||
|
@ -51,64 +51,6 @@ select:disabled {
|
|||||||
color: #444;
|
color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
white-space: nowrap;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.field > * {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-right: none;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
.field .label {
|
|
||||||
background-color: #eee;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.field > :first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
|
||||||
.field > :last-child {
|
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
|
||||||
.field > input,
|
|
||||||
.field > textarea,
|
|
||||||
.field > select {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.field > input:focus,
|
|
||||||
.field > textarea:focus,
|
|
||||||
.field > select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #00008b;
|
|
||||||
box-shadow: 0 0 0 3px rgba(0, 0, 139, 0.2);
|
|
||||||
}
|
|
||||||
.field > input.invalid,
|
|
||||||
.field > textarea.invalid,
|
|
||||||
.field > select.invalid {
|
|
||||||
background-color: rgba(255, 80, 80, 0.3);
|
|
||||||
border-color: rgb(255, 80, 80);
|
|
||||||
}
|
|
||||||
.field > span.checkbox {
|
|
||||||
text-align: center;
|
|
||||||
min-width: 75px;
|
|
||||||
}
|
|
||||||
.field > select {
|
|
||||||
appearance: none;
|
|
||||||
padding: 0.5rem 2rem 0.5rem 0.5rem;
|
|
||||||
background: #fff;
|
|
||||||
background-image: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.5rem center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
background-color: #00008b;
|
background-color: #00008b;
|
||||||
border: 1px solid #00008b;
|
border: 1px solid #00008b;
|
||||||
@ -143,6 +85,78 @@ select:disabled {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.field > * {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.field .label,
|
||||||
|
.field span.checkbox {
|
||||||
|
background-color: #eee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.field > input,
|
||||||
|
.field > textarea,
|
||||||
|
.field > select {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.field > input:focus,
|
||||||
|
.field > textarea:focus,
|
||||||
|
.field > select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #00008b;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 0, 139, 0.2);
|
||||||
|
}
|
||||||
|
.field > input.invalid,
|
||||||
|
.field > textarea.invalid,
|
||||||
|
.field > select.invalid {
|
||||||
|
background-color: rgba(255, 80, 80, 0.3);
|
||||||
|
border-color: rgb(255, 80, 80);
|
||||||
|
}
|
||||||
|
.field.inline {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
.field.inline > input,
|
||||||
|
.field.inline > textarea,
|
||||||
|
.field.inline > select {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
.field > span.checkbox {
|
||||||
|
min-width: 75px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.field > select {
|
||||||
|
appearance: none;
|
||||||
|
padding: 0.5rem 2rem 0.5rem 0.5rem;
|
||||||
|
background: #fff;
|
||||||
|
background-image: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
}
|
||||||
|
.field > :first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
}
|
||||||
|
.field > *:not(:last-child) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.field > :last-child {
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
code,
|
code,
|
||||||
.code {
|
.code {
|
||||||
font-family: Menlo, monospace;
|
font-family: Menlo, monospace;
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { environment } from './stores';
|
import { environment } from './stores';
|
||||||
|
|
||||||
export function resolveKeypath(object, path) {
|
// Calculate the min and max values of signed integers with n bits
|
||||||
// Get a value from an object with a JSON path, from Webdesq core
|
export const intMin = bits => Math.pow(2, bits - 1) * -1;
|
||||||
|
export const intMax = bits => Math.pow(2, bits - 1) - 1;
|
||||||
|
export const uintMax = bits => Math.pow(2, bits) - 1;
|
||||||
|
|
||||||
|
// Boundaries for some ubiquitous integer types
|
||||||
|
export const int32 = [ intMin(32), intMax(32) ];
|
||||||
|
export const int64 = [ intMin(64), intMax(64) ];
|
||||||
|
export const uint64 = [ 0, uintMax(64) ];
|
||||||
|
|
||||||
|
// Get a value from an object with a JSON path, from Webdesq core
|
||||||
|
export function resolveKeypath(object, path) {
|
||||||
const parts = path.split('.').flatMap(part => {
|
const parts = path.split('.').flatMap(part => {
|
||||||
const indexMatch = part.match(/\[\d+\]/g);
|
const indexMatch = part.match(/\[\d+\]/g);
|
||||||
if (indexMatch) {
|
if (indexMatch) {
|
||||||
@ -23,6 +32,42 @@ export function resolveKeypath(object, path) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set a value in an object with a JSON path, from Webdesq core
|
||||||
|
export function setValue(object, path, value) {
|
||||||
|
const parts = path.split('.').flatMap(part => {
|
||||||
|
let indexMatch = part.match(/\[\d+\]/g);
|
||||||
|
if (indexMatch) {
|
||||||
|
// Convert strings to numbers
|
||||||
|
const indexes = indexMatch.map(index => Number(index.slice(1, -1)));
|
||||||
|
const base = part.slice(0, part.indexOf(indexMatch[0]));
|
||||||
|
return base.length ? [ base, ...indexes ] : indexes;
|
||||||
|
}
|
||||||
|
indexMatch = part.match(/^\d+$/g);
|
||||||
|
if (indexMatch) {
|
||||||
|
// Convert strings to numbers
|
||||||
|
const indexes = indexMatch.map(index => Number(index.slice(1, -1)));
|
||||||
|
const base = part.slice(0, part.indexOf(indexMatch[0]));
|
||||||
|
return base.length ? [ base, ...indexes ] : indexes;
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
let result = object;
|
||||||
|
while (parts.length) {
|
||||||
|
const part = parts.shift();
|
||||||
|
if (!parts.length) {
|
||||||
|
// No parts left, we can set the value
|
||||||
|
result[part] = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!result[part]) {
|
||||||
|
// Default value if none is found
|
||||||
|
result[part] = (typeof parts[0] === 'number') ? [] : {};
|
||||||
|
}
|
||||||
|
result = result[part];
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
export function controlKeyDown(event) {
|
export function controlKeyDown(event) {
|
||||||
const env = get(environment);
|
const env = get(environment);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -34,7 +79,16 @@ export function controlKeyDown(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function randInt(min, max) {
|
// https://stackoverflow.com/a/14794066
|
||||||
|
export function isInt(value) {
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const x = parseFloat(value);
|
||||||
|
return (x | 0) === x;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randInt(min, max) {
|
||||||
return Math.round(Math.random() * (max - min) + min);
|
return Math.round(Math.random() * (max - min) + min);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,15 +11,30 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ViewType string
|
type ViewType string
|
||||||
|
type InputType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TableView ViewType = "table"
|
TableView ViewType = "table"
|
||||||
ListView ViewType = "list"
|
ListView ViewType = "list"
|
||||||
|
|
||||||
|
NoInput InputType = "none"
|
||||||
|
StringInput InputType = "string"
|
||||||
|
ObjectIdInput InputType = "objectid"
|
||||||
|
IntegerInput InputType = "int"
|
||||||
|
LongInput InputType = "long"
|
||||||
|
Uint64Input InputType = "uint64"
|
||||||
|
DoubleInput InputType = "double"
|
||||||
|
DecimalInput InputType = "decimal"
|
||||||
|
BoolInput InputType = "bool"
|
||||||
|
DateInput InputType = "date"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewColumn struct {
|
type ViewColumn struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Width int64 `json:"width"`
|
Width int64 `json:"width"`
|
||||||
|
ShowInTable bool `json:"showInTable"`
|
||||||
|
Mandatory bool `json:"mandatory"`
|
||||||
|
InputType InputType `json:"inputType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type View struct {
|
type View struct {
|
||||||
@ -82,48 +97,6 @@ func (a *App) Views() (ViewStore, error) {
|
|||||||
return views, nil
|
return views, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (a *App) AddView(jsonData string) error {
|
|
||||||
// views, err := a.Views()
|
|
||||||
// if err != nil {
|
|
||||||
// runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
|
||||||
// Type: runtime.InfoDialog,
|
|
||||||
// Title: "Could not retrieve views",
|
|
||||||
// })
|
|
||||||
// return errors.New("could not retrieve existing view store")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var newView View
|
|
||||||
// err = json.Unmarshal([]byte(jsonData), &newView)
|
|
||||||
// if err != nil {
|
|
||||||
// runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
|
||||||
// Type: runtime.InfoDialog,
|
|
||||||
// Title: "Malformed JSON",
|
|
||||||
// })
|
|
||||||
// return errors.New("invalid JSON")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// id, err := uuid.NewRandom()
|
|
||||||
// if err != nil {
|
|
||||||
// runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
|
||||||
// Type: runtime.InfoDialog,
|
|
||||||
// Title: "Failed to generate a UUID",
|
|
||||||
// })
|
|
||||||
// return errors.New("could not generate a UUID")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// views[id.String()] = newView
|
|
||||||
// err = updateViewStore(views)
|
|
||||||
// if err != nil {
|
|
||||||
// runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
|
||||||
// Type: runtime.InfoDialog,
|
|
||||||
// Title: "Could not update view store",
|
|
||||||
// })
|
|
||||||
// return errors.New("could not update view store")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (a *App) UpdateViewStore(jsonData string) error {
|
func (a *App) UpdateViewStore(jsonData string) error {
|
||||||
var viewStore ViewStore
|
var viewStore ViewStore
|
||||||
err := json.Unmarshal([]byte(jsonData), &viewStore)
|
err := json.Unmarshal([]byte(jsonData), &viewStore)
|
Loading…
Reference in New Issue
Block a user