diff --git a/packages/default-packages/unigraph.hotkeys/executables/openOmnibarAction.js b/packages/default-packages/unigraph.hotkeys/executables/openOmnibarAction.js index 865be460..8212b71a 100644 --- a/packages/default-packages/unigraph.hotkeys/executables/openOmnibarAction.js +++ b/packages/default-packages/unigraph.hotkeys/executables/openOmnibarAction.js @@ -1,5 +1,5 @@ unigraph.getState('global/omnibarSummoner').setValue({ - show: true, + show: !unigraph.getState('global/omnibarSummoner').value?.show, tooltip: '', defaultValue: '', }); diff --git a/packages/unigraph-dev-backend/src/server.ts b/packages/unigraph-dev-backend/src/server.ts index 255e9db1..2fe1169c 100644 --- a/packages/unigraph-dev-backend/src/server.ts +++ b/packages/unigraph-dev-backend/src/server.ts @@ -927,6 +927,7 @@ export default async function startServer(client: DgraphClient) { }; await Promise.all(Object.values(caches).map((el: Cache) => el.updateNow())); + updateClientCache(serverStates, 'schemaMap', serverStates.caches.schemas.data); initEntityHeads( serverStates, diff --git a/packages/unigraph-dev-explorer/src/examples/bookmarks/Bookmarks.tsx b/packages/unigraph-dev-explorer/src/examples/bookmarks/Bookmarks.tsx index b05c2873..e31bf4c4 100644 --- a/packages/unigraph-dev-explorer/src/examples/bookmarks/Bookmarks.tsx +++ b/packages/unigraph-dev-explorer/src/examples/bookmarks/Bookmarks.tsx @@ -63,7 +63,7 @@ export const createBookmark = async (text: string, add = true) => { return { name, children: tags.map((tagName) => ({ - type: { 'unigraph.id': '$/schema/subentity' }, + type: { 'unigraph.id': '$/schema/interface/semantic' }, _value: { type: { 'unigraph.id': '$/schema/tag' }, name: tagName, diff --git a/packages/unigraph-dev-explorer/src/examples/notes/NoteEditor.tsx b/packages/unigraph-dev-explorer/src/examples/notes/NoteEditor.tsx index d726a73c..2c597498 100644 --- a/packages/unigraph-dev-explorer/src/examples/notes/NoteEditor.tsx +++ b/packages/unigraph-dev-explorer/src/examples/notes/NoteEditor.tsx @@ -8,6 +8,11 @@ import { parseUnigraphHtml } from '../../clipboardUtils'; import { getParentsAndReferences } from '../../components/ObjectView/backlinksUtils'; import { inlineTextSearch, inlineObjectSearch } from '../../components/UnigraphCore/InlineSearchPopup'; import { debounce, scrollIntoViewIfNeeded, selectUid, setCaret, TabContext } from '../../utils'; +import { + GetChangesForAutoComplete, + changesForOpenScopedChar, + changesForOpenScopedMarkdownLink, +} from '../../utils/autocomplete'; import { htmlToMarkdown } from '../semantic/Markdown'; import { permanentlyDeleteBlock } from './commands'; import { addTextualCommand, applyCommand } from './history'; @@ -29,45 +34,6 @@ const TextareaAutosizeStyled = styled(TextareaAutosize)(({ theme }) => ({ }, })); -type ScopeForAutoComplete = { currentText: string; caret: number; middle: string; end: string }; -type ChangesForAutoComplete = { newText: string; newCaret: number; newCaretOffset: number }; -type GetChangesForAutoComplete = (scope: ScopeForAutoComplete, ev: KeyboardEvent) => ChangesForAutoComplete; - -const changesForOpenScopedChar = ( - { currentText, caret, middle, end }: ScopeForAutoComplete, - ev: KeyboardEvent, -): ChangesForAutoComplete => { - const isChar = (c: string) => c !== ' ' && c !== '\n' && c !== '\t' && c !== undefined; - const nextChar = currentText?.[caret]; - const shouldNotOpen = middle.length === 0 && isChar(nextChar) && nextChar !== closeScopeCharDict[ev.key]; - - return { - newText: `${currentText.slice(0, caret)}${ev.key}${middle}${ - shouldNotOpen ? '' : closeScopeCharDict[ev.key] - }${end}${currentText.slice(caret + (middle + end).length)}`, - newCaret: caret + 1, - newCaretOffset: middle.length, - }; -}; -const changesForOpenScopedMarkdownLink = (scope: ScopeForAutoComplete, ev: KeyboardEvent): ChangesForAutoComplete => { - const { currentText, caret, middle, end } = scope; - - if (isUrl(middle)) { - return { - newText: `${currentText.slice(0, caret)}[](${middle})${end}${currentText.slice( - caret + (middle + end).length, - )}`, - newCaret: caret + 1, - newCaretOffset: 0, - }; - } - return { - newText: `${currentText.slice(0, caret)}[${middle}]()${end}${currentText.slice(caret + (middle + end).length)}`, - newCaret: caret + middle.length + 3, - newCaretOffset: 0, - }; -}; - const touchParents = (data: any) => { const [parents] = getParentsAndReferences(data['~_value'], data['unigraph.origin']); if (!data._hide) parents.push({ uid: data.uid }); @@ -426,6 +392,7 @@ export const useNoteEditor: (...args: any) => [any, (text: string) => void, () = [callbacks], ); + // TODO: merge all these into autocomplete.ts in utils const handleScopedAutoComplete = React.useCallback( (changeTextAndCaret: GetChangesForAutoComplete, ev: KeyboardEvent) => { ev.preventDefault(); diff --git a/packages/unigraph-dev-explorer/src/examples/semantic/Markdown.tsx b/packages/unigraph-dev-explorer/src/examples/semantic/Markdown.tsx index ee2e8182..a36f9bf8 100644 --- a/packages/unigraph-dev-explorer/src/examples/semantic/Markdown.tsx +++ b/packages/unigraph-dev-explorer/src/examples/semantic/Markdown.tsx @@ -60,29 +60,30 @@ const addPage = async (childrenRoot: any, newName: any, subsId: any) => { }, '$/schema/note_block', ); - window.unigraph.updateObject( - childrenRoot.uid, - { - _value: { - children: { - '_value[': [ - { - _key: `[[${newName}]]`, - _value: { - 'dgraph.type': ['Interface'], - type: { 'unigraph.id': '$/schema/interface/semantic' }, - _hide: true, - _value: { uid: newUid[0] }, + if (childrenRoot.uid) + window.unigraph.updateObject( + childrenRoot.uid, + { + _value: { + children: { + '_value[': [ + { + _key: `[[${newName}]]`, + _value: { + 'dgraph.type': ['Interface'], + type: { 'unigraph.id': '$/schema/interface/semantic' }, + _hide: true, + _value: { uid: newUid[0] }, + }, }, - }, - ], + ], + }, }, }, - }, - true, - false, - subsId, - ); + true, + false, + subsId, + ); return newUid[0]; }; @@ -103,7 +104,7 @@ const tryFindLinkToAdd = async (childrenRoot: any, name: string, subsId: any) => }`, ]); const linkFound = res[0].sort((a: any, b: any) => b.popularity - a.popularity)[0]; - if (linkFound) { + if (linkFound && childrenRoot.uid) { console.log('k'); window.unigraph.updateObject( childrenRoot.uid, diff --git a/packages/unigraph-dev-explorer/src/examples/semantic/Tag.tsx b/packages/unigraph-dev-explorer/src/examples/semantic/Tag.tsx index 8de0324c..bd24cf43 100644 --- a/packages/unigraph-dev-explorer/src/examples/semantic/Tag.tsx +++ b/packages/unigraph-dev-explorer/src/examples/semantic/Tag.tsx @@ -10,7 +10,7 @@ import { getContrast } from '../../utils'; const getBgColor = (tag: any) => (tag?.color?.startsWith && tag.color.startsWith('#') ? tag.color : 'unset'); export const Tag: DynamicViewRenderer = ({ data, callbacks }) => { - const [tag, setTag] = React.useState(() => (data._value ? unpad(data) : data)); + const tag = data._value ? unpad(data) : data; return ( { const parsed = parseTodoObject(inputStr, refs); + console.log(parsed); if (!preview) { // eslint-disable-next-line no-return-await const uid = await window.unigraph.addObject(parsed, '$/schema/todo'); diff --git a/packages/unigraph-dev-explorer/src/init.tsx b/packages/unigraph-dev-explorer/src/init.tsx index 54dff05b..f01ff589 100644 --- a/packages/unigraph-dev-explorer/src/init.tsx +++ b/packages/unigraph-dev-explorer/src/init.tsx @@ -80,6 +80,11 @@ export function init(hostname?: string) { browserId, }; + // Set up basic anonymized analytics by default + if (window.localStorage.getItem('enableAnalytics') === null) { + window.localStorage.setItem('enableAnalytics', 'true'); + } + let userSettings = defaultSettings; if (!isJsonString(window.localStorage.getItem('userSettings'))) { @@ -297,10 +302,10 @@ function initBacklinkManager() { } /** - * Initializes analytics when user explicitly opted in. + * Initializes analytics depends on user opt-in status * * Information we collect: - * - basic info: time, email (user-given), browser, os, country + * - basic info: time, email (user-given, with explicit permission), browser, os, country * - user actions: click, keypress - anonimized into 15 second chunks * * We use them to calculate session length and usage frequency. diff --git a/packages/unigraph-dev-explorer/src/pages/SearchOverlay.tsx b/packages/unigraph-dev-explorer/src/pages/SearchOverlay.tsx index 6eb67147..45e9b484 100644 --- a/packages/unigraph-dev-explorer/src/pages/SearchOverlay.tsx +++ b/packages/unigraph-dev-explorer/src/pages/SearchOverlay.tsx @@ -8,6 +8,7 @@ import { AutoDynamicView } from '../components/ObjectView/AutoDynamicView'; import { inlineTextSearch } from '../components/UnigraphCore/InlineSearchPopup'; import { parseQuery } from '../components/UnigraphCore/UnigraphSearch'; import { isElectron, trivialTypes, typeHasDynamicView } from '../utils'; +import { handleOpenScopedChar } from '../utils/autocomplete'; const groups = [ { @@ -40,33 +41,32 @@ function AdderComponent({ input, setInput, open, setClose, callback, summonerToo if (allAdders[parsedInput.key]) { allAdders[parsedInput.key].adder(parsedInput.val).then((res: any) => { const [object, type] = res; - window.unigraph.getSchemas().then((schemas: any) => { - try { - const padded = buildUnigraphEntity(JSON.parse(JSON.stringify(object)), type, schemas); - setToAdd( -
{ - ev.stopPropagation(); + const schemas = (window.unigraph as any).getSchemaMap(); + try { + const padded = buildUnigraphEntity(JSON.parse(JSON.stringify(object)), type, schemas); + setToAdd( +
{ + ev.stopPropagation(); + }} + > + - -
, - ); - } catch (e) { - console.log(e); - } - }); + /> +
, + ); + } catch (e) { + console.log(e); + } }); } }, [parsedInput]); @@ -83,6 +83,12 @@ function AdderComponent({ input, setInput, open, setClose, callback, summonerToo }} inputRef={tf} value={input} + onKeyDown={(ev) => { + if (ev.key === '[') + handleOpenScopedChar(ev as any, tf, input, (ipt: string) => { + setInput(ipt); + }); + }} onChange={(ev) => { const newContent = ev.target.value; const caret = ev.target.selectionStart || 0; @@ -392,6 +398,10 @@ export function SearchOverlayPopover({ open, setClose, noShadow }: any) { else setSearchEnabled(false); }, [summonerState]); + React.useEffect(() => { + setSearchEnabled(!!summonerState.show); + }, [summonerState]); + React.useEffect(() => { if (searchEnabled) { const listener = (event: MouseEvent) => { diff --git a/packages/unigraph-dev-explorer/src/pages/Settings.tsx b/packages/unigraph-dev-explorer/src/pages/Settings.tsx index 45931ea1..1ad23873 100644 --- a/packages/unigraph-dev-explorer/src/pages/Settings.tsx +++ b/packages/unigraph-dev-explorer/src/pages/Settings.tsx @@ -95,32 +95,67 @@ export default function Settings() { Analytics + false} + key="analyticsOptedIn" + > + + + { + analyticsState.setValue(!analyticsMode); + }} + checked={analyticsMode} + inputProps={{ + 'aria-labelledby': 'switch-list-label-optin-analytics-mode', + }} + /> + + false} key="analytics"> - Opt-in to analytics with mixpanel by entering your email address. -
- We will only record your usage length and basic information (OS, country). - + !analyticsMode ? ( + + Opt-in to analytics by clicking "Opt-in".
+ Optionally, you can enter your email address before clicking to associate your + analytics information with your email. +
+ We will only record your usage length and basic information (OS, country). +
+ ) : ( + + Associate your analytics information with your email (optional).
+ This will allow us to understand how you use Unigraph, and contact you when things + are broken. +
+ ) } /> setEmail(ev.target.value)} />
diff --git a/packages/unigraph-dev-explorer/src/utils/autocomplete.ts b/packages/unigraph-dev-explorer/src/utils/autocomplete.ts new file mode 100644 index 00000000..e80d5c2a --- /dev/null +++ b/packages/unigraph-dev-explorer/src/utils/autocomplete.ts @@ -0,0 +1,88 @@ +import { isUrl } from 'unigraph-dev-common/lib/utils/utils'; +import { setCaret } from '../utils'; + +export const closeScopeCharDict: { [key: string]: string } = { + '[': ']', + '(': ')', + '"': '"', + '`': '`', + $: '$', + // '{':'}', +}; + +export type ScopeForAutoComplete = { currentText: string; caret: number; middle: string; end: string }; +export type ChangesForAutoComplete = { newText: string; newCaret: number; newCaretOffset: number }; +export type GetChangesForAutoComplete = (scope: ScopeForAutoComplete, ev: KeyboardEvent) => ChangesForAutoComplete; + +export const changesForOpenScopedChar = ( + { currentText, caret, middle, end }: ScopeForAutoComplete, + ev: KeyboardEvent, +): ChangesForAutoComplete => { + const isChar = (c: string) => c !== ' ' && c !== '\n' && c !== '\t' && c !== undefined; + const nextChar = currentText?.[caret]; + const shouldNotOpen = middle.length === 0 && isChar(nextChar) && nextChar !== closeScopeCharDict[ev.key]; + + return { + newText: `${currentText.slice(0, caret)}${ev.key}${middle}${ + shouldNotOpen ? '' : closeScopeCharDict[ev.key] + }${end}${currentText.slice(caret + (middle + end).length)}`, + newCaret: caret + 1, + newCaretOffset: middle.length, + }; +}; +export const changesForOpenScopedMarkdownLink = ( + scope: ScopeForAutoComplete, + ev: KeyboardEvent, +): ChangesForAutoComplete => { + const { currentText, caret, middle, end } = scope; + + if (isUrl(middle)) { + return { + newText: `${currentText.slice(0, caret)}[](${middle})${end}${currentText.slice( + caret + (middle + end).length, + )}`, + newCaret: caret + 1, + newCaretOffset: 0, + }; + } + return { + newText: `${currentText.slice(0, caret)}[${middle}]()${end}${currentText.slice(caret + (middle + end).length)}`, + newCaret: caret + middle.length + 3, + newCaretOffset: 0, + }; +}; + +export const handleScopedAutoComplete = ( + changeTextAndCaret: GetChangesForAutoComplete, + ev: KeyboardEvent, + textInputRef: React.RefObject, + currentText: string, + setInput: any, +) => { + ev.preventDefault(); + const caret = (textInputRef.current as any).selectionStart; + let middle = document.getSelection()?.toString() || ''; + let end = ''; + if (middle.endsWith(' ')) { + middle = middle.slice(0, middle.length - 1); + end = ' '; + } + const { newText, newCaret, newCaretOffset } = changeTextAndCaret({ currentText, caret, middle, end }, ev); + setInput(newText); + setTimeout(() => setCaret(document, textInputRef.current, newCaret, newCaretOffset), 0); + (textInputRef.current as any).dispatchEvent( + new Event('change', { + bubbles: true, + cancelable: true, + }), + ); + return newText; +}; +export const handleOpenScopedChar = (ev: KeyboardEvent, textInputRef: any, currentText: string, setInput: any) => + handleScopedAutoComplete(changesForOpenScopedChar, ev, textInputRef, currentText, setInput); +export const handleOpenScopedMarkdownLink = ( + ev: KeyboardEvent, + textInputRef: any, + currentText: string, + setInput: any, +) => handleScopedAutoComplete(changesForOpenScopedMarkdownLink, ev, textInputRef, currentText, setInput);