diff --git a/frontend/src/actions.js b/frontend/src/actions.js index 3a9ba0c..ac3da8c 100644 --- a/frontend/src/actions.js +++ b/frontend/src/actions.js @@ -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: diff --git a/frontend/src/components/clock.svelte b/frontend/src/components/clock.svelte new file mode 100644 index 0000000..6ae78fd --- /dev/null +++ b/frontend/src/components/clock.svelte @@ -0,0 +1,70 @@ + + + + + + + {#each [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 ] as minute} + + + {#each [ 1, 2, 3, 4 ] as offset} + + {/each} + {/each} + + + + + + + + + + + + + + + diff --git a/frontend/src/components/datepicker.svelte b/frontend/src/components/datepicker.svelte new file mode 100644 index 0000000..98d30a4 --- /dev/null +++ b/frontend/src/components/datepicker.svelte @@ -0,0 +1,201 @@ + + + +
+
+
+ + + +
+ + + + + + {#each daysAbbr as dayName} + + {/each} + + + + {#each calendar as week} + + + {#each week as day} + + {/each} + + {/each} +
{dayName}
{getWeek(week[0])} + +
+
+ +
+
+ + : + + : + +
+ +
+
+ + +
+ + diff --git a/frontend/src/components/forminput.svelte b/frontend/src/components/forminput.svelte new file mode 100644 index 0000000..2203217 --- /dev/null +++ b/frontend/src/components/forminput.svelte @@ -0,0 +1,136 @@ + + +
+
+ {#if type === 'string'} + + {:else if type === 'objectid'} + + {:else if numericInputTypes.includes(type)} + + {:else if type === 'bool'} + + {:else if type === 'date'} + showDatepicker = true} /> + {/if} +
+ +
+ {#if type === 'objectid'} + {#if objectIdInput?.disabled} + + {/if} + + {:else if type === 'date'} + + + {/if} + +
+
+ +{#if type === 'date'} + +{/if} + + diff --git a/frontend/src/components/grid-items.svelte b/frontend/src/components/grid-items.svelte index 6786d7b..d0ac33f 100644 --- a/frontend/src/components/grid-items.svelte +++ b/frontend/src/components/grid-items.svelte @@ -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 @@ } -{#each _items as item} +{#each _items as item, index} 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 @@ {#each columns as column, columnIndex} - {@const value = column.key?.includes('.') ? resolveKeypath(item, column.key) : item[column.key]} - -
- {formatValue(value)} -
+ + {#if column.inputType} + + {:else} +
+ {formatValue(keypathProxies[index][column.key])} +
+ {/if} {/each} + + {#if canRemoveItems} + + + + {/if} {#if item.children && childrenOpen[item[key]]} + 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'); + }
@@ -38,6 +49,10 @@ {#each columns as column} {column.title || ''} {/each} + + {#if canRemoveItems} + + {/if} {/if} @@ -48,13 +63,24 @@ {columns} {key} {striped} + {canSelect} + {canRemoveItems} {hideObjectIndicators} {hideChildrenToggles} bind:activePath + bind:inputsValid on:select on:trigger /> + + {#if canAddRows} + + + + {/if}
@@ -86,8 +112,10 @@ th { font-weight: 600; text-align: left; - } - th { padding: 2px; } + + /* tfoot button { + margin-top: 0.5rem; + } */ diff --git a/frontend/src/components/icon.svelte b/frontend/src/components/icon.svelte index 100c8ea..88c8ea0 100644 --- a/frontend/src/components/icon.svelte +++ b/frontend/src/components/icon.svelte @@ -73,5 +73,11 @@ {:else if name === 'target'} + {:else if name === 'trash'} + + {:else if name === 'anchor'} + + {:else if name === 'o'} + {/if} diff --git a/frontend/src/components/objectgrid.svelte b/frontend/src/components/objectgrid.svelte index 136636b..f171e15 100644 --- a/frontend/src/components/objectgrid.svelte +++ b/frontend/src/components/objectgrid.svelte @@ -1,5 +1,6 @@ {#if displayOnly} {#if items.length} {#if draggable && isArray} - dragstart(e, kp, data)} draggable="true" class="bracket" on:click={clicked} tabindex="0">{openBracket} + onDragstart(e, kp, data)} draggable="true" class="bracket" on:click={onClick} tabindex="0">{openBracket} {:else} - {openBracket} + {openBracket} {/if}
    (readonly ? displayOnly = true : displayOnly = false)} > {#each items as i, idx}
  • {#if !isArray} {#if draggable} - dragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="key">{i}: + onDragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="key">{i}: {:else} {i}: {/if} @@ -107,7 +110,7 @@ {:else} {#if draggable} - dragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="val {getType(data[i])}">{format(data[i])}{#if idx < items.length - 1},{/if} + onDragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="val {getType(data[i])}">{format(data[i])}{#if idx < items.length - 1},{/if} {:else} {format(data[i])}{#if idx < items.length - 1},{/if} {/if} @@ -115,16 +118,14 @@
  • {/each}
- {closeBracket}{#if !last}, - {/if} + {closeBracket}{#if !last},{/if}
- {openBracket}{collapsedSymbol}{closeBracket}{#if !last && collapsed},{/if} + {openBracket}{collapsedSymbol}{closeBracket}{#if !last && collapsed},{/if} {:else} {@html isArray ? '[]' : '{}'} {/if} {:else} - + {/if} diff --git a/frontend/src/organisms/connection/collection/components/form.svelte b/frontend/src/organisms/connection/collection/components/form.svelte index 73cb13a..5e20194 100644 --- a/frontend/src/organisms/connection/collection/components/form.svelte +++ b/frontend/src/organisms/connection/collection/components/form.svelte @@ -1,7 +1,7 @@ {#if item && view} - {#each view?.columns?.filter(c => c.inputType !== 'none') || [] as column} + {#each view?.columns?.filter(c => inputTypes.includes(c.inputType)) || [] as column} {/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); diff --git a/frontend/src/organisms/connection/collection/components/forminput.svelte b/frontend/src/organisms/connection/collection/components/forminput.svelte deleted file mode 100644 index 92ce207..0000000 --- a/frontend/src/organisms/connection/collection/components/forminput.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - -
- {#if type === 'string'} - - {:else if numericTypes.includes(type)} - - {:else if type === 'bool'} - - {:else if type === 'date'} - {@const isNotDate = !isDate(value)} - - - {/if} -
- - diff --git a/frontend/src/organisms/connection/collection/components/viewconfig.svelte b/frontend/src/organisms/connection/collection/components/viewconfig.svelte index 62a68c9..0406dad 100644 --- a/frontend/src/organisms/connection/collection/components/viewconfig.svelte +++ b/frontend/src/organisms/connection/collection/components/viewconfig.svelte @@ -151,7 +151,7 @@ - + diff --git a/frontend/src/organisms/connection/collection/insert.svelte b/frontend/src/organisms/connection/collection/insert.svelte index a97a871..3914932 100644 --- a/frontend/src/organisms/connection/collection/insert.svelte +++ b/frontend/src/organisms/connection/collection/insert.svelte @@ -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, {} ]; + }
@@ -64,10 +70,29 @@ use:input={{ type: 'json', autofocus: true }} > - {:else} + {:else if viewType === 'form'}
+ {:else if viewType === 'table'} +
+ 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} + /> +
{/if}
@@ -114,6 +139,11 @@ gap: 0.5rem; } + .table { + background-color: #fff; + border: 1px solid #ccc; + } + .flex { display: flex; justify-content: space-between; diff --git a/frontend/src/organisms/settings/index.svelte b/frontend/src/organisms/settings/index.svelte index 75da366..6d21596 100644 --- a/frontend/src/organisms/settings/index.svelte +++ b/frontend/src/organisms/settings/index.svelte @@ -22,7 +22,7 @@ - +
diff --git a/frontend/src/style.css b/frontend/src/style.css index 2de111a..4a6dbb7 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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; diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 3924d1f..3059319 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -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; + } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7b63c0c --- /dev/null +++ b/package-lock.json @@ -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==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e297aa8 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "date-fns": "^2.29.3" + } +}