├── .gitignore ├── .prettierrc ├── .DS_Store ├── logo.png ├── src ├── tailwind.css ├── utils.ts ├── handleClosePopup.ts ├── insertUUID.ts ├── insertTemplatedBlock.ts ├── searchbar.tsx ├── App.tsx ├── parser.tsx ├── index.tsx ├── inserterUI.tsx └── App.css ├── .github ├── .DS_Store └── workflows │ └── publish.yml ├── .postcssrc ├── tailwind.config.js ├── index.html ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache 3 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sawhney17/logseq-smartblocks/HEAD/.DS_Store -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sawhney17/logseq-smartblocks/HEAD/logo.png -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sawhney17/logseq-smartblocks/HEAD/.github/.DS_Store -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | "tailwindcss": {}, 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/handleClosePopup.ts: -------------------------------------------------------------------------------- 1 | export const handleClosePopup = () => { 2 | // ESC 3 | document.addEventListener( 4 | 'keydown', 5 | function (e) { 6 | if (e.keyCode === 27) { 7 | logseq.hideMainUI({ restoreEditingCursor: true }); 8 | } 9 | e.stopPropagation(); 10 | }, 11 | false 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{vue,js,ts,jsx,tsx,hbs,html}'], 3 | darkMode: 'media', // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | spacing: { 7 | 100: '50rem', 8 | }, 9 | }, 10 | }, 11 | variants: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /src/insertUUID.ts: -------------------------------------------------------------------------------- 1 | import '@logseq/libs' 2 | 3 | export function persistUUID(uuid) { 4 | logseq.Editor.getBlock(uuid, { includeChildren: true }).then( 5 | (result) => { 6 | if (!result.content.match(/(id:: .*)/g)){ 7 | const finalContent = result.content + `\nid:: ${result.uuid}` 8 | logseq.Editor.updateBlock(result.uuid, finalContent) 9 | } 10 | }) 11 | 12 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "noImplicitAny": false, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["./src"] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Aryan Sawhney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logseq-smartblocks-plugin", 3 | "version": "3.5.1", 4 | "description": "A plugin to replicate Smartblocks", 5 | "author": "@sawhney17", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "parcel ./index.html --public-url ./", 9 | "build": "parcel build --public-url . --no-source-maps index.html" 10 | }, 11 | "devDependencies": { 12 | "@logseq/libs": "^0.0.6", 13 | "@types/react": "^17.0.38", 14 | "@types/react-dom": "^17.0.11", 15 | "autoprefixer": "^10.4.2", 16 | "parcel": "^2.0.0", 17 | "postcss": "^8.1.0" 18 | }, 19 | "dependencies": { 20 | "@emotion/react": "^11.9.0", 21 | "@emotion/styled": "^11.8.1", 22 | "@mui/material": "latest", 23 | "autoprefix": "^1.0.1", 24 | "axios": "^0.26.1", 25 | "logseq-dateutils": "latest", 26 | "postcss-cli": "^9.1.0", 27 | "postcss-import": "^14.0.2", 28 | "react": "^17.0.2", 29 | "react-dom": "^17.0.2", 30 | "react-tooltip": "^4.2.21", 31 | "sherlockjs": "^1.4.2", 32 | "tailwindcss": "^3.0.15" 33 | }, 34 | "logseq": { 35 | "id": "logseq-smartblocks", 36 | "main": "dist/index.html", 37 | "icon": "./logo.png" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build plugin 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10 8 | 9 | env: 10 | PLUGIN_NAME: logseq-templater-plugin 11 | APIKEY: ${{ secrets.APIKEY }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '16.x' # You might need to adjust this value to your own version 23 | - name: Build 24 | id: build 25 | env: 26 | APIKEY: ${{ secrets.APIKEY }} 27 | run: | 28 | npm i && npm run build 29 | mkdir ${{ env.PLUGIN_NAME }} 30 | cp README.md package.json logo.png ${{ env.PLUGIN_NAME }} 31 | mv dist ${{ env.PLUGIN_NAME }} 32 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 33 | ls 34 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 35 | - name: Create Release 36 | uses: ncipollo/release-action@v1 37 | id: create_release 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | VERSION: ${{ github.ref }} 41 | with: 42 | allowUpdates: true 43 | draft: false 44 | prerelease: false 45 | 46 | - name: Upload zip file 47 | id: upload_zip 48 | uses: actions/upload-release-asset@v1 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | upload_url: ${{ steps.create_release.outputs.upload_url }} 53 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 54 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 55 | asset_content_type: application/zip 56 | 57 | - name: Upload package.json 58 | id: upload_metadata 59 | uses: actions/upload-release-asset@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | upload_url: ${{ steps.create_release.outputs.upload_url }} 64 | asset_path: ./package.json 65 | asset_name: package.json 66 | asset_content_type: application/json 67 | -------------------------------------------------------------------------------- /src/insertTemplatedBlock.ts: -------------------------------------------------------------------------------- 1 | import '@logseq/libs' 2 | import { 3 | BlockEntity, 4 | IBatchBlock, 5 | } from '@logseq/libs/dist/LSPlugin.user'; 6 | 7 | import { parseDynamically } from './parser'; 8 | 9 | import { renderApp, valueArray } from './index'; 10 | export var networkRequest = false 11 | export var stopStatus = true 12 | export function editNetworkRequest(value) { 13 | networkRequest = value 14 | } 15 | 16 | export var data = null 17 | // const reg = /<%([^%].*?)%>/g 18 | const reg = /<%(.*?)%>/g 19 | export var blockUuid2 20 | export var sibling 21 | var currentRun = 1 22 | var previousRun = 0 23 | async function triggerParse(obj) { 24 | if (obj.content) { 25 | let regexMatched = obj.content.match(reg) 26 | for (const x in regexMatched) { 27 | let toBeParsed = obj.content 28 | var currentMatch = regexMatched[x] 29 | let formattedMatch = await parseDynamically(currentMatch); 30 | let newRegexString = toBeParsed.replace(currentMatch, formattedMatch) 31 | obj.content = newRegexString 32 | obj.properties = {} 33 | } 34 | } 35 | currentRun += 1 36 | await obj.children.map(triggerParse) 37 | } 38 | 39 | export function triggerParseInitially(obj) { 40 | console.log(obj) 41 | if (obj.content) { 42 | 43 | let regexMatched = obj.content.match(reg) 44 | // delete obj.uuid 45 | // delete obj.id 46 | // delete obj.left 47 | // delete obj.parent 48 | // delete obj.pathRefs 49 | for (const x in regexMatched) { 50 | var currentMatch = regexMatched[x] 51 | if (currentMatch.toLowerCase().includes("setinput:")) { 52 | const inputs = currentMatch.slice(2, -2).split(":") 53 | const variableName = inputs[1] 54 | const variableOptions = inputs[2]?.split(",") 55 | variableOptions ? valueArray.push({ value: "", name: variableName, options: variableOptions }) : valueArray.push({ value: "", name: variableName }) 56 | } 57 | } 58 | } 59 | obj.children.map(triggerParseInitially) 60 | } 61 | 62 | export async function insertProperlyTemplatedBlock(blockUuid3, template2, sibling3) { 63 | var query = ` 64 | [:find (pull ?b [*]) 65 | :where 66 | [?b :block/properties ?p] 67 | [(get ?p :template) ?ty] 68 | [(= "${template2}" ?ty)]]` 69 | blockUuid2 = blockUuid3 70 | sibling = sibling3 71 | let refUUID 72 | try { 73 | let ret = await logseq.DB.datascriptQuery(query) 74 | const results = ret?.flat() 75 | if (results && results.length > 0) { 76 | refUUID = results[0].uuid 77 | console.log(refUUID) 78 | console.log(results) 79 | let origBlock = await logseq.Editor.getBlock(refUUID, { 80 | includeChildren: true, 81 | }) 82 | data = origBlock 83 | console.log("origBlock") 84 | console.log(origBlock) 85 | triggerParseInitially(origBlock) 86 | if (valueArray.length > 0) { 87 | renderApp() 88 | logseq.showMainUI() 89 | } 90 | else { 91 | logseq.hideMainUI({ restoreEditingCursor: true }); 92 | insertProperlyTemplatedBlock2(blockUuid3, sibling3, origBlock) 93 | } 94 | 95 | } 96 | } catch (error) { 97 | } 98 | } 99 | export async function insertProperlyTemplatedBlock2(blockUuid, sibling2, origBlock) { 100 | data = origBlock 101 | async function insertFinally() { 102 | let page = await logseq.Editor.getPage(blockUuid) 103 | if (page != undefined) { 104 | console.log( 105 | "iserting" 106 | ) 107 | let blockTree = (await logseq.Editor.getPageBlocksTree(blockUuid)) 108 | let lastBlock = blockTree[blockTree.length - 1] 109 | logseq.Editor.insertBatchBlock(lastBlock.uuid, data.children as unknown as IBatchBlock, { sibling: true }) 110 | } 111 | else { 112 | logseq.Editor.insertBatchBlock(blockUuid, data.children as unknown as IBatchBlock, { sibling: (sibling2 === 'true') }) 113 | } 114 | 115 | } 116 | 117 | triggerParse(data) 118 | timeOutShouldBeSet() 119 | function checkDiff() { 120 | 121 | if (currentRun != previousRun) { 122 | previousRun = currentRun 123 | timeOutShouldBeSet() 124 | } 125 | else { 126 | if (networkRequest == true) { 127 | setTimeout(function () { 128 | checkDiff() 129 | networkRequest = false 130 | }, 500); 131 | } 132 | else { 133 | logseq.App.showMsg("Run has begun") 134 | insertFinally() 135 | } 136 | } 137 | } 138 | function timeOutShouldBeSet() { 139 | setTimeout(function () { 140 | checkDiff() 141 | }, 100); 142 | } 143 | if (origBlock.children.length === 0 || !origBlock.children) { 144 | logseq.App.showMsg("Whoops! Doesn't look like there's any content under the template."); 145 | } 146 | } 147 | // } 148 | // catch (err) { 149 | // console.log(err) 150 | // } 151 | // } -------------------------------------------------------------------------------- /src/searchbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./App.css"; 4 | import "@logseq/libs"; 5 | import "./tailwind.css"; 6 | import { insertProperlyTemplatedBlock } from "./insertTemplatedBlock"; 7 | 8 | export let smartblocks; 9 | 10 | export async function updateTemplates() { 11 | let query = ` 12 | [:find (pull ?b [*]) 13 | :where 14 | [?b :block/properties ?p] 15 | [(get ?p :template)]] 16 | `; 17 | let result = await logseq.DB.datascriptQuery(query) 18 | smartblocks = result.map((item) => item[0].properties.template); 19 | return result.map((item) => item[0].properties.template); 20 | } 21 | const SearchBar: React.FC<{ blockID }> = ({ blockID }) => { 22 | const [searchTerm, setSearchTerm] = React.useState(""); 23 | const [searchResults, setSearchResults] = React.useState([]); 24 | const [highlightedResult, setHighlightedResult] = React.useState(null); 25 | const firstUpdate = useRef(true); 26 | 27 | React.useEffect(() => { 28 | setTimeout(() => { 29 | const input = document.getElementById("smartblockSearchbar"); 30 | if (input) { 31 | input.focus(); 32 | } 33 | }, 100); 34 | }, []); 35 | const handleChange = (event) => { 36 | setSearchTerm(event.target.value); 37 | }; 38 | React.useEffect(() => { 39 | let results; 40 | updateTemplates(); 41 | if (searchTerm != "") { 42 | results = smartblocks.filter((template) => 43 | template.toLowerCase().includes(searchTerm) 44 | ); 45 | } else { 46 | results = smartblocks; 47 | } 48 | setSearchResults(results); 49 | }, [searchTerm]); 50 | React.useEffect(() => { 51 | if (firstUpdate.current) { 52 | firstUpdate.current = false; 53 | return; 54 | } 55 | 56 | setHighlightedResult(0); 57 | }, [searchResults]); 58 | 59 | React.useEffect(() => { 60 | if (firstUpdate.current) { 61 | return; 62 | } 63 | document.addEventListener("keydown", keyControl); 64 | updateHighlight(); 65 | return () => document.removeEventListener("keydown", keyControl); 66 | }, [highlightedResult]); 67 | function keyControl(e) { 68 | if (e.keyCode === 40) { 69 | //down arrow 70 | if (highlightedResult < searchResults.length - 1) { 71 | let hello = highlightedResult + 1; 72 | setHighlightedResult(hello); 73 | } else { 74 | setHighlightedResult(0); 75 | } 76 | } 77 | if (e.keyCode === 38) { 78 | //Up arrow 79 | if (highlightedResult > 0) { 80 | setHighlightedResult(highlightedResult - 1); 81 | } else { 82 | setHighlightedResult(searchResults.length - 1); 83 | } 84 | } 85 | if (e.keyCode === 13) { 86 | //EnterKey arrow 87 | insertProperlyTemplatedBlock( 88 | blockID, 89 | searchResults[highlightedResult], 90 | "true" 91 | ); 92 | } 93 | e.handled = true; 94 | } 95 | const insertBlocks = (e) => { 96 | insertProperlyTemplatedBlock(blockID, e.target.id, "true"); 97 | }; 98 | const updateHighlight = () => { 99 | for (const x in searchResults) { 100 | if (x == highlightedResult) { 101 | document.getElementById(searchResults[x]).classList.add("bg-[#4c4c4c]"); 102 | } else { 103 | document 104 | .getElementById(searchResults[x]) 105 | .classList.remove("bg-[#4c4c4c]"); 106 | } 107 | } 108 | }; 109 | 110 | return ( 111 |
112 |
113 |
114 | 122 |
    123 | {searchResults.map((item) => ( 124 |
    129 |
    134 | T 135 |
    136 |
  • 140 | {item} 141 |
  • 142 |
    143 | ))} 144 |
145 |
146 |
147 |
148 | ); 149 | }; 150 | 151 | export default SearchBar; 152 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { getDateForPage } from "logseq-dateutils"; 3 | import { valueArray, valueZero, editValueArray } from "./index"; 4 | import "./App.css"; 5 | import { 6 | data, 7 | insertProperlyTemplatedBlock2, 8 | blockUuid2, 9 | sibling, 10 | } from "./insertTemplatedBlock"; 11 | import { Autocomplete } from "@mui/material"; 12 | 13 | var replacementArray = {}; 14 | function triggerParse(obj) { 15 | obj.properties = {}; 16 | const reg = /<%setInput:([^%].*?)%>/gi; 17 | const reg2 = /<%getInput:([^%].*?)%>/gi; 18 | if (obj.content) { 19 | var newRegexString = obj.content; 20 | const regexMatched = obj.content.match(reg); 21 | for (const x in regexMatched) { 22 | const toBeParsed = newRegexString; 23 | var currentMatch = regexMatched[x]; 24 | const variableName = currentMatch.slice(2, -2).split(":")[1]; 25 | newRegexString = toBeParsed.replace( 26 | currentMatch, 27 | replacementArray[variableName] 28 | ); 29 | } 30 | const regexMatched2 = obj.content.match(reg2); 31 | for (const x in regexMatched2) { 32 | const toBeParsed = newRegexString; 33 | var currentMatch = regexMatched2[x]; 34 | const variableName = currentMatch.slice(2, -2).split(":")[1]; 35 | newRegexString = toBeParsed.replace( 36 | currentMatch, 37 | replacementArray[variableName] 38 | ); 39 | } 40 | obj.content = newRegexString; 41 | } 42 | obj.children.map(triggerParse); 43 | } 44 | 45 | export interface FormValues { 46 | name: string 47 | value: string 48 | options?: string[] 49 | } 50 | const App = () => { 51 | //Add type annotation to the state 52 | const [formValues, setFormValues] = useState([]); 53 | const [isOpened, setIsOpened] = useState(true); 54 | const [isSubmitted, setIsSubmitted] = useState(false); 55 | const [something, setSomething] = useState(false); 56 | 57 | //force remount the component upon formValues changing 58 | React.useEffect(() => { 59 | setFormValues(formValues); 60 | console.log(something) 61 | } 62 | , [formValues]); 63 | let resetExit = (event) => { 64 | event.preventDefault(); 65 | setIsSubmitted(false); 66 | setIsOpened(true); 67 | setFormValues([]); 68 | valueZero(); 69 | logseq.hideMainUI({ restoreEditingCursor: true }); 70 | }; 71 | let updateUI2 = () => { 72 | let newFormValues = [...formValues]; 73 | for (const x in valueArray) { 74 | console.log(valueArray) 75 | newFormValues.push(valueArray[x]); 76 | } 77 | console.log(formValues) 78 | setFormValues(newFormValues); 79 | setIsSubmitted(true); 80 | setIsOpened((wasOpened) => !wasOpened); 81 | }; 82 | let handleChange = (i, e) => { 83 | let newFormValues = [...formValues]; 84 | newFormValues[i]["value"] = e.target.value; 85 | setFormValues(newFormValues); 86 | }; 87 | let handleChange2 = (i, e) => { 88 | let newFormValues = [...formValues]; 89 | newFormValues[i]["value"] = e.target.value; 90 | setFormValues(newFormValues); 91 | } 92 | 93 | let handleSubmit = (event) => { 94 | event.preventDefault(); 95 | setIsSubmitted(false); 96 | setIsOpened(true); 97 | setFormValues([]); 98 | editValueArray(formValues); 99 | logseq.hideMainUI({ restoreEditingCursor: true }); 100 | for (const x in valueArray) { 101 | //if value is empty and there exists options, use the first option instead 102 | if (valueArray[x].value === "" && valueArray[x].options) { 103 | valueArray[x].value = valueArray[x].options[0]; 104 | } 105 | const value = valueArray[x].value; 106 | const name = valueArray[x].name; 107 | replacementArray[name] = value; 108 | } 109 | triggerParse(data); 110 | valueZero(); 111 | insertProperlyTemplatedBlock2(blockUuid2, sibling, data); 112 | }; 113 | 114 | document.addEventListener( 115 | "keydown", 116 | function (e) { 117 | if (e.keyCode === 27) { 118 | // logseq.hideMainUI({ restoreEditingCursor: true }); 119 | resetExit(e); 120 | } 121 | e.stopPropagation(); 122 | }, 123 | false 124 | ); 125 | return ( 126 |
127 |
128 |
129 |
130 | {formValues.map((element, index) => { 131 | console.log(element) 132 | return ( 133 |
137 | 138 | {element.options ? ( 139 | 152 | ) : ( 153 | handleChange(index, e)}> 158 | 159 | ) 160 | // }) 161 | // } 162 | } 163 |
164 | ) 165 | })} 166 |
167 | {isOpened && } 168 |
169 | {/* {isOpened && ( */} 170 | 177 | {/* )} */} 178 | {isSubmitted && ( 179 | 182 | )} 183 | {isOpened && ( 184 | 191 | )} 192 |
193 |
194 |
195 |
196 | ); 197 | }; 198 | 199 | export default App; 200 | -------------------------------------------------------------------------------- /src/parser.tsx: -------------------------------------------------------------------------------- 1 | import { getDateForPage } from "logseq-dateutils"; 2 | import Sherlock from "sherlockjs"; 3 | import { persistUUID } from "./insertUUID"; 4 | 5 | import axios from "axios"; 6 | import { blockUuid2, editNetworkRequest } from "./insertTemplatedBlock"; 7 | // set APIKEY to be equal to the api key from github secrets 8 | const APIKEY = process.env.APIKEY; 9 | async function parseRandomly(pageName: string) { 10 | pageName.toLowerCase(); 11 | let query = `[:find (pull ?b [*]) 12 | :where 13 | [?b :block/path-refs [:block/name "${pageName.toLowerCase()}"]]]`; 14 | 15 | let results = await logseq.DB.datascriptQuery(query); 16 | let flattenedResults = results.map((mappedQuery) => ({ 17 | uuid: mappedQuery[0].uuid, 18 | })); 19 | 20 | let index = Math.floor(Math.random() * flattenedResults.length); 21 | persistUUID(flattenedResults[index].uuid); 22 | return `((${flattenedResults[index].uuid}))`; 23 | } 24 | 25 | function parseWeather(data, format) { 26 | let emojiArray = { 27 | Clear: "🔆", 28 | Clouds: "🌥", 29 | Atmosphere: "🌫", 30 | Snow: "❄️", 31 | Rain: "🌧", 32 | Drizzle: "🌧", 33 | Thunderstorm: "⛈", 34 | } 35 | let temperature; 36 | if (format == "f") { 37 | temperature = (data.main.temp - 273.15) * 9/5 + 32 38 | temperature = Math.round((temperature + Number.EPSILON) * 100) / 100 39 | } else { 40 | temperature = data.main.temp - 273.15 41 | temperature = Math.round((temperature + Number.EPSILON) * 100) / 100 42 | } 43 | return `${temperature}°${emojiArray[data.weather[0].main]}`; 44 | } 45 | function parseConditional(condition: string, value) { 46 | switch (condition) { 47 | case "dayofweek": 48 | if (new Date().getDay() == value) { 49 | return "Success"; 50 | } else if (new Date().getDay() == 0 && value == 7) { 51 | return "Success"; 52 | } else { 53 | return "Error"; 54 | } 55 | case "dayofmonth": 56 | if (new Date().getDate() == value) { 57 | return "Success"; 58 | } else { 59 | return "Error"; 60 | } 61 | case "month": 62 | if (new Date().getMonth() == value) { 63 | return "Success"; 64 | } else { 65 | return "Error"; 66 | } 67 | case "dayofyear": 68 | if (new Date().getDate() == value) { 69 | return "Success"; 70 | } else { 71 | return "Error"; 72 | } 73 | default: 74 | return "Error"; 75 | } 76 | } 77 | 78 | export function parseVariablesOne(template) {} 79 | export async function parseDynamically(blockContent) { 80 | 81 | const userConfigs = await logseq.App.getUserConfigs(); 82 | const preferredDateFormat = userConfigs.preferredDateFormat; 83 | let currentTime = new Date(); 84 | let ifParsing = /(i+f)/gi; 85 | let pageBlock = /currentpage/gi; 86 | let uuid = /randUUID/gi; 87 | let randomParsing = /randomblock/; 88 | let shouldNotEncodeURL = true 89 | let weatherQuery = /weather/; 90 | let parsedInput; 91 | if (blockContent.match("<%%")){ 92 | parsedInput = blockContent.slice(3, -2); 93 | shouldNotEncodeURL = false; 94 | } 95 | else { 96 | parsedInput = blockContent.slice(2, -2); 97 | } 98 | if (blockContent.match(ifParsing)) { 99 | let input = parsedInput.split(":"); 100 | let spaceParsedInput = input[0].replace(/\s+/g, ""); 101 | let input1 = spaceParsedInput.split("||"); 102 | let tempStorageArray = []; 103 | for (const x in input1) { 104 | let input2 = input1[x].split("if"); 105 | let input3 = input2[1].split("="); 106 | tempStorageArray.push(parseConditional(input3[0], input3[1])); 107 | } 108 | if (tempStorageArray.includes("Success")) { 109 | return input[1]; 110 | } else { 111 | return ""; 112 | } 113 | } 114 | 115 | if (blockContent.match(weatherQuery)) { 116 | try { 117 | 118 | let spacedSplit = parsedInput.split(" "); 119 | let weatherLocation = spacedSplit[1]; 120 | let weatherFormat = spacedSplit[0].split("weather")[1]; 121 | try { 122 | editNetworkRequest(true); 123 | let url = `http://api.openweathermap.org/geo/1.0/direct?q=${weatherLocation}&limit=1&appid=${APIKEY}`; 124 | let locationLongLat = await axios.get(url); 125 | let lon = locationLongLat.data[0].lon; 126 | let lat = locationLongLat.data[0].lat; 127 | 128 | let url2 = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${APIKEY}`; 129 | 130 | let weatherData = await axios.get(url2); 131 | return parseWeather(weatherData.data, weatherFormat); 132 | } catch(error) { 133 | console.log(error) 134 | return "Oops, unable to reach servers"; 135 | 136 | } 137 | } catch { 138 | return "Oops, no location provided"; 139 | } 140 | 141 | } 142 | if (blockContent.toLowerCase().match(pageBlock)) { 143 | // let currentp3age = await logseq.Editor.getCurrentPage(); 144 | let currentp3age = await logseq.Editor.getPage((await logseq.Editor.getBlock(blockUuid2)).page.id) 145 | const inputSplit = parsedInput.split(":") 146 | if (inputSplit.length > 1) { 147 | return shouldNotEncodeURL? parseProperties((inputSplit[1]), currentp3age): encodeURIComponent(parseProperties((inputSplit[1]), currentp3age)) 148 | } 149 | else{ 150 | if (currentp3age != null) { 151 | return shouldNotEncodeURL? currentp3age.originalName: encodeURIComponent(currentp3age.name); 152 | } else { 153 | return shouldNotEncodeURL? getDateForPage(currentTime, preferredDateFormat): encodeURIComponent(getDateForPage(currentTime, preferredDateFormat)); 154 | }} 155 | } 156 | if (blockContent.match(uuid)){ 157 | return ("wxy" + Math.random().toString(36).slice(2)) 158 | } 159 | if (blockContent.match(randomParsing)) { 160 | // let spaceParsedInput = parsedInput.replace(/\s+/g, ''); 161 | let input2 = parsedInput.split("randomblock"); 162 | let input3 = input2[1].replace(" ", ""); 163 | return shouldNotEncodeURL? await parseRandomly(input3): encodeURIComponent(await parseRandomly(input3)); 164 | } 165 | 166 | // Implement time parsing 167 | if ( 168 | blockContent.toLowerCase() == "<%currentTime%>" || 169 | blockContent.toLowerCase() == "<%time%>" || 170 | blockContent.toLowerCase() == "<%current time%>" 171 | ) { 172 | let formattedTime; 173 | if (currentTime.getMinutes() < 10) { 174 | formattedTime = 175 | currentTime.getHours() + ":" + "0" + currentTime.getMinutes(); 176 | } else { 177 | formattedTime = currentTime.getHours() + ":" + currentTime.getMinutes(); 178 | } 179 | return shouldNotEncodeURL ? formattedTime : encodeURIComponent(formattedTime); 180 | } 181 | // Implement if parsing 182 | const parsedBlock = await Sherlock.parse(blockContent); 183 | // Destructure 184 | const { startDate } = parsedBlock; 185 | 186 | if (startDate == null) { 187 | return blockContent; 188 | } 189 | return shouldNotEncodeURL ? getDateForPage(startDate, preferredDateFormat): encodeURIComponent(getDateForPage(startDate, preferredDateFormat)); 190 | } 191 | 192 | function parseProperties(text, currentPage) { 193 | const updatedText = text.replace(" ", "") 194 | //Convert dash case to camel case 195 | const camelCaseText = updatedText.replace(/-([a-z])/g, function (g) { 196 | return g[1].toUpperCase(); 197 | }); 198 | const propertyList = currentPage.properties[camelCaseText] 199 | if (propertyList != undefined ){ 200 | return propertyList.toString() 201 | } 202 | else { 203 | return `No property exists for key ${camelCaseText}` 204 | } 205 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { IBatchBlock, SettingSchemaDesc } from "@logseq/libs/dist/LSPlugin.user"; 3 | import Sherlock from "sherlockjs"; 4 | import { getDateForPage, getDateForPageWithoutBrackets } from "logseq-dateutils"; 5 | import React from "react"; 6 | import ReactDOM from "react-dom"; 7 | import App, { FormValues } from "./App"; 8 | import { insertProperlyTemplatedBlock } from "./insertTemplatedBlock"; 9 | import { updateTemplates } from "./searchbar"; 10 | import SearchBar from "./searchbar"; 11 | import { handleClosePopup } from "./handleClosePopup"; 12 | import InsertionUI from "./inserterUI"; 13 | import { uuidRegex } from "./utils"; 14 | /* 15 | * main entry 16 | */ 17 | 18 | export function renderApp (){ 19 | handleClosePopup() 20 | ReactDOM.render( 21 | 22 | 23 | , 24 | document.getElementById("app") 25 | );} 26 | 27 | let settings: SettingSchemaDesc[] = [ 28 | { 29 | key: "Keyboard-Shortcut", 30 | type: "string", 31 | title: "Keyboard Shortcut for Triggering Smartblocks", 32 | description: "keyboard shortcut to trigger smartblock insertion window", 33 | default: "mod+t" 34 | } 35 | ] 36 | async function checkTemplate(uuid) { 37 | //Credits to Alex for this implementation https://github.com/QWxleA 38 | //is block(uuid) on a template? 39 | try { 40 | let block = await logseq.Editor.getBlock(uuid); 41 | let checkTPL = 42 | block.properties && block.properties.template != undefined ? true : false; 43 | let checkPRT = 44 | block.parent != null && block.parent.id !== block.page.id ? true : false; 45 | 46 | if (checkTPL === false && checkPRT === false) return false; 47 | if (checkTPL === true) return true; 48 | return await checkTemplate(block.parent.id); 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | } 53 | 54 | export var valueCount = 0; 55 | export var valueArray: FormValues[] = []; 56 | export var currentValueCount = 0; 57 | export var currentValueArray = []; 58 | 59 | export function editValueArray(value: FormValues[]) { 60 | valueArray = value; 61 | } 62 | export function valueZero() { 63 | valueArray = []; 64 | } 65 | async function main() { 66 | updateTemplates() 67 | // logseq.App.onCurrentGraphChanged(updateTemplates) 68 | logseq.useSettingsSchema(settings); 69 | logseq.provideModel({ 70 | async insertTemplatedBlock(e: any) { 71 | const { blockUuid, template, sibling, location } = e.dataset; 72 | let blockUuid2: string = blockUuid 73 | console.log(location) 74 | console.log(location.match(uuidRegex)) 75 | if (location == "" || location == "undefined"){ 76 | blockUuid2 = blockUuid 77 | console.log(`THis is the fun${location}`) 78 | console.log(location.match(uuidRegex)) 79 | } 80 | else if (location.match(uuidRegex)!= null || (await logseq.Editor.getPage(location)!= undefined)){ 81 | blockUuid2 = location 82 | console.log(`THis is the ${location}`) 83 | } 84 | else { 85 | const parsedBlock = await Sherlock.parse(location); 86 | const { isAllDay, eventTitle, startDate, endDate } = parsedBlock; 87 | blockUuid2 = getDateForPageWithoutBrackets(startDate, (await logseq.App.getUserConfigs()).preferredDateFormat) 88 | console.log(`THis is not the ${location}`) 89 | } 90 | console.log(blockUuid2) 91 | insertProperlyTemplatedBlock(blockUuid2, template, sibling); 92 | }, 93 | }), 94 | logseq.provideStyle(` 95 | .templater-btn { 96 | border: 1px solid var(--ls-border-color); 97 | white-space: initial; 98 | padding: 2px 4px; 99 | border-radius: 4px; 100 | user-select: none; 101 | cursor: default; 102 | display: flex; 103 | align-content: center; 104 | } 105 | 106 | .templater-btn:hover { 107 | background-color: #defcf0; 108 | border-color: #9ddbc7; 109 | color: #0F9960; 110 | } 111 | `); 112 | logseq.Editor.registerSlashCommand("Create Inline SmartBlock", async () => { 113 | await logseq.Editor.insertAtEditingCursor( 114 | `{{renderer :smartblockInline, }} ` 115 | ); 116 | // templaterBlock = await logseq.Editor.getCurrentBlock(); 117 | }); 118 | logseq.Editor.registerSlashCommand("Insert Smartblock", async (e) => { 119 | updateTemplates(); 120 | handleClosePopup() 121 | ReactDOM.render( 122 | 123 | 124 | , 125 | document.getElementById("app") 126 | ); 127 | logseq.showMainUI(); 128 | console.log("Insert Smartblock"); 129 | // templaterBlock = await logseq.Editor.getCurrentBlock(); 130 | }); 131 | logseq.Editor.registerSlashCommand( 132 | "Create Inline SmartBlock(guided)", 133 | async () => { 134 | await logseq.Editor.insertAtEditingCursor( 135 | `{{renderer :smartblockInline, template name, sibling?}} ` 136 | ); 137 | // templaterBlock = await logseq.Editor.getCurrentBlock(); 138 | } 139 | ); 140 | logseq.App.registerCommandPalette({ 141 | key: 'Toggle Smartblock Inserter', 142 | label: 'Select and insert Smartblock', 143 | keybinding: { 144 | binding: logseq.settings["Keyboard-Shortcut"], 145 | mode: 'global', 146 | } 147 | }, async (e) => { 148 | updateTemplates().then(()=>{ 149 | if (e.uuid != null){ 150 | updateTemplates(); 151 | handleClosePopup() 152 | ReactDOM.render( 153 | 154 | 155 | , 156 | document.getElementById("app") 157 | ) 158 | logseq.showMainUI() 159 | } 160 | else { 161 | logseq.UI.showMsg("Error: not in a block") 162 | } 163 | }) 164 | 165 | }); 166 | 167 | logseq.Editor.registerSlashCommand("Create SmartBlock Button", async (e) => { 168 | updateTemplates(); 169 | ReactDOM.render( 170 | 171 | 172 | , 173 | document.getElementById("app") 174 | ) 175 | logseq.showMainUI() 176 | handleClosePopup() 177 | // templaterBlock = await logseq.Editor.getCurrentBlock(); 178 | }); 179 | 180 | logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => { 181 | updateTemplates(); 182 | var [type, template, title, sibling, location] = payload.arguments; 183 | if (title == undefined) { 184 | title = "New Template"; 185 | } 186 | let realSiblings; 187 | if (sibling == "true") { 188 | realSiblings = true; 189 | } else { 190 | realSiblings = false; 191 | } 192 | if (type == ":smartblock") { 193 | logseq.provideUI({ 194 | key: `${slot}`, 195 | reset: true, 196 | slot, 197 | template: ` 198 | 200 | `, 201 | }); 202 | } 203 | if (type == ":smartblockInline") { 204 | if (!(await checkTemplate(payload.uuid))) { 205 | logseq.Editor.updateBlock(payload.uuid, ""); 206 | await insertProperlyTemplatedBlock(payload.uuid, template, title).then( 207 | () => { 208 | logseq.Editor.updateBlock(payload.uuid, ""); 209 | } 210 | ); 211 | } else { 212 | logseq.provideUI({ 213 | key: `${slot}`, 214 | reset: true, 215 | slot, 216 | template: ` 217 | 218 | `, 219 | }); 220 | } 221 | } 222 | }); 223 | 224 | 225 | } 226 | 227 | logseq.ready(main).catch(console.error); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >If this plugin helps you, I'd really appreciate your support. You can [buy me a coffee here. ](https://www.buymeacoffee.com/sawhney17) 2 | # SmartBlocks for Logseq 3 | ![GitHub all releases](https://img.shields.io/github/downloads/sawhney17/logseq-smartblocks/total) ![version](https://img.shields.io/github/package-json/v/sawhney17/logseq-smartblocks) 4 | 5 | This plugin enables advanced templating workflows within logseq. 6 | 7 | Currently porting features from the official Smartblocks from Roam plugin. Feel free to ping me on twitter [@aryansawhney17](https://twitter.com/aryansawhney17), or file a github issue for any feedback or feature requests. 8 | 9 | 10 | # Features 11 | 12 | ## Template Inserter 13 | ### Setup 14 | 1. Use the slash menu and type `Create smartblock button block` 15 | 2. Update the values through the input 16 | 17 | ### Usage in action 18 | ![Screen Recording 2022-01-16 at 5 19 50 PM](https://user-images.githubusercontent.com/80150109/149662222-79f0fa35-c2d8-4070-93d9-a39b0b7b4982.gif) 19 | ![Screen Recording 2022-01-30 at 12 48 51 AM](https://user-images.githubusercontent.com/80150109/151677540-a9b24fdd-3139-42c5-bfb8-8d0ad967dd84.gif) 20 | ![Screen Recording 2022-03-31 at 10 54 10 AM](https://user-images.githubusercontent.com/80150109/161001870-3dac3eae-8e61-4c40-9568-144f01f401d9.gif) 21 | 22 | 23 | ### Expand from smartblock expander 24 | - In version 3.0.0, you can now expand and select a smartblock _while_ inserting, similar to how Logseq's templates work. 25 | - Simply type mod+t or /insert smartblocks and navigate to the desired Smartblock and then hit enter or click it 26 | - You can search to filter in this view 27 | ### Types of expansion 28 | - You can either expand by creating a templater button or create an inline smartblock expansion 29 | - Running smartblocks expansion templater 30 | - Use the slash command to insert templater or templater guided 31 | - Follow the below syntax for flags and setup of the original template 32 | - Running smartblocks expansion inline 33 | - Use the slash command to insert templater inline 34 | - Follows slightly different syntax for flags with the format being `{{renderer :smartblockInline, templateName, sibling?}}` 35 | - Sample: `{{renderer :smartblockInline, journalTemplate, true}}` 36 | - When this is in a block marked template, then this won't auto expand to allow for usage in regular logseq templates or as a daily note template 37 | ### Flags 38 | - You can configure the templater in the following ways 39 | 1. Set template(required) 40 | 2. Set title(required for button) 41 | 3. Set sibling true or false(required for button) 42 | - Whether you want the template to be inserted as a sibling(as a new bullet _not_ a child of the button), or as a child (indented under the button) 43 | - Basic structure of the button expansion is as such 44 | - `{{renderer :smartblock, journalTemplate, New Journal, false}}` 45 | 46 | ### Accessing page properties 47 | ### Encode URL method 48 | 1. A useful application for SmartBlocks is automatically generating URLs or iFrame embeds based on things like the page name 49 | 2. In this case it becomes useful to format variables into page names. 50 | 3. Hence, by simply adding a second `%` you can have it encoded into URL format. 51 | - `<%%currentPage%>` 52 | ### Using NLP 53 | 1. In the templates, wherever you want a dynamic date, one that shows a different value based on the date it was inserted, use this syntax `<%NLP Input%>` 54 | - `<%Today%>` 55 | - `<%Last monday%>` 56 | - `<%current time%>` 57 | - `<%30th of december%>` 58 | 2. Automatically respects your date settings 59 | - Your format will automatically generate a specific date 60 | 3. Support Aliases 61 | - `[tomorrow](<%tomorrow%>)` in the template generates `[tomorrow]([[Jan 19th, 2022]])` 62 | ![Screen Recording 2022-01-18 at 12 36 29 PM](https://user-images.githubusercontent.com/80150109/149903174-1187c911-76c3-44be-87dc-a35e5fb37d5a.gif) 63 | ### Using if statements with dates 64 | 1. If you want an item in a template to only show up on a certain day of the week 65 | 2. Follow the below syntax 66 | - if the day of the week is monday 67 | - `<%if dayofweek = 1: text to be entered%>` 68 | - if it's the 22th 69 | - `<%if dayofmonth = 22: text to be entered%>` 70 | - if it's the 100th day of the year 71 | - `<%if dayofyear = 100: text to be entered%>` 72 | - if it's July 73 | - `<%month%> = 7: text to be entered` 74 | 3. As of the latest release, you can now have OR statements to result in insertion if *any* of the properties are in action. Simply separate the parameters with `||`. Some examples are as follows 75 | - if it's either january or a monday 76 | - `<%if month = 1|| if dayofweek = 1 : text to be entered%>` 77 | - if it's a weekend 78 | - `<%if dayofweek = 6|| if dayofweek = 7 : text to be entered%>` 79 | ### Using the Random Function 80 | - Limited scope at the moment, can currently fetch a random block linking to a page and create a reference to the random block 81 | - Use `` 82 | -`` 83 | -`` 84 | 85 | ### Call renderers using randUUID 86 | - If you'd like to integrate things like the wordcount plugin, you can do so by generating a random alphanumeric using using <%randUUID%> 87 | - {{renderer :wordcount_<%randUUID%>}} will insert a new word counter whenever the smartblock is called provided the word counter plugin is installed 88 | ### Set inputs and use variables 89 | - You can ask the user for inputs. You can then reuse this input multiple times in the smartblock 90 | - To set an input, use `` 91 | - If you'd like you give a dropdown list of options, use `<%setInput: variableName:comma,separated,options%>` 92 | - To get the input of an already set input, i.e. if you want to use something twice, do `<%getInput: variableName%>` 93 | ### Using the weather function 94 | - Makes it possible to grab the current weather from Open Weather Map API 95 | - Format used is `<%weatherf Dubai%>` 96 | - Start with weather 97 | - Add the desired format, either `f` for farenheight or `c` for celcius 98 | - Add your current location, don't be too speciifc, weather data *may* not always be available for more specific searches 99 | - Format returned is `30° 🌧` 100 | ### Using the current Page function 101 | - If you want to import the current page into the template as a dynamic variable, simply insert the placeholder `<%currentPage%>` 102 | - Study <%currentPage%> on <%tomorrow%> 103 | - Will return `Study [[Nuclear Physics]] [[Feb 3, 2022]]` 104 | 105 | ### Limitations 106 | - ~~Only works with dynamic variables up to 4 blocks deep~~ fixed in latest update 107 | 108 | ### TODO 109 | - [x] Enable support for natural language processing for dates allowing for dynamic dates (different date auto added based on current date) 110 | - [x] Allow user to set variables via inputs 111 | - [ ] Allow NLP dates in if statements 112 | 113 | ### Using variables and inputs 114 | ![Screen Recording 2022-02-08 at 11 58 41 AM](https://user-images.githubusercontent.com/80150109/152961013-3dd95af1-beb3-45ad-9f12-4b62176517df.gif) 115 | 116 | - If you update to the latest version, you can create inputs and variables 117 | - To set an input use the variable <%setinput: inputName%> 118 | - When you call the smartblock, you will then be able to define inputs for those inputs, they will auto replace these blocks 119 | - To access the property of an existing input, let's say you've already set the input `people` using <%setinput: people%> 120 | - You can now access the already defined input via the line <%getinput: people%> without it prompting you again. 121 | - You can even pass dynamic variables like <%current page%> as an input! 122 | ## Property Renderer 123 | Has been shifted to it's own plugin: https://github.com/sawhney17/logseq-property-visualizer 124 | 125 | 126 | # Credits 127 | > If you like the work I'm doing, consider [buying me a coffee](https://www.buymeacoffee.com/sawhney17) :) 128 | - [Sherlockjs](https://github.com/neilgupta/Sherlock) 129 | - Thanks a ton to [hkgnp](https://github.com/hkgnp) and his [NLP plugin](https://github.com/hkgnp/logseq-datenlp-plugin) for implementation inspiration 130 | - Credits to the original SmartBlocks plugin for Roam by [@tfthacker](https://twitter.com/tfthacker) 131 | - Thanks to OpenWeatherMap for weather data 132 | -------------------------------------------------------------------------------- /src/inserterUI.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./App.css"; 3 | import "@logseq/libs"; 4 | import ReactTooltip from "react-tooltip"; 5 | import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; 6 | import "./tailwind.css"; 7 | import { smartblocks } from "./searchbar"; 8 | import TextField from "@mui/material/TextField"; 9 | import { uuidRegex } from "./utils"; 10 | 11 | 12 | 13 | 14 | const InsertionUI: React.FC<{ blockUUID }> = (blockUUID) => { 15 | const [location, setLocation] = React.useState(null); 16 | const [sibling, setSibling] = React.useState("false"); 17 | const [title, setTitle] = React.useState(""); 18 | const [smartblockIndex, setSmartblockIndex] = React.useState(1) 19 | const handleChange = (e) => { 20 | console.log(e.target.id) 21 | if (e.target.id == "locationInput") { 22 | setLocation(e.target.value) 23 | } 24 | if (e.target.id == "titleInput") { 25 | setTitle(e.target.value) 26 | } 27 | 28 | if (e.target.id == "siblignSelect") { 29 | setSibling(e.target.value) 30 | } 31 | if (e.target.id == "templateSelection") { 32 | setSmartblockIndex(e.target.value) 33 | } 34 | } 35 | 36 | const [pagesList, setPageList] = React.useState([]) 37 | const filter = createFilterOptions<[AutocompleteProps]>(); 38 | //Fetch pages upon first load 39 | React.useEffect(() => { 40 | logseq.DB.datascriptQuery(`[:find (pull ?p [*]) :where [?p :block/uuid ?u][?p :block/name]]`).then((pages) => { 41 | let pages2 = pages.map((page) => { 42 | console.log(page[0]) 43 | return { title: page[0]["original-name"] } 44 | }) 45 | console.log(pages2) 46 | setPageList(pages2) 47 | } 48 | ) 49 | } 50 | , []) 51 | 52 | 53 | const handleSubmit = async () => { 54 | console.log(location) 55 | let finalString; 56 | 57 | let actualLocation = location !== null ? location.title 58 | .replaceAll("(", "") 59 | .replaceAll(")", "") 60 | .replace('Block with uuid: ', "") 61 | .replace("NLP Parse: ", "") 62 | : null; 63 | let newTitle = title 64 | if (title == "") { 65 | newTitle = "New Smartblock" 66 | } 67 | console.log(smartblockIndex) 68 | console.log(smartblocks) 69 | if (actualLocation !== null) { 70 | 71 | finalString = `{{renderer :smartblock, ${smartblocks[smartblockIndex]}, ${newTitle}, ${sibling}, ${actualLocation}}}` 72 | } 73 | else { 74 | finalString = `{{renderer :smartblock, ${smartblocks[smartblockIndex]}, ${newTitle}, ${sibling}}}` 75 | } 76 | console.log(blockUUID.blockUUID) 77 | // let block = (await logseq.Editor.getBlock(blockUUID.blockUUID)).content 78 | // logseq.Editor.updateBlock(blockUUID.blockUUID, `${block} ${finalString}`) 79 | logseq.Editor.insertAtEditingCursor(finalString) 80 | logseq.hideMainUI() 81 | } 82 | return ( 83 |
84 |
85 |

Insert Smartblock

86 |
87 |
88 |
89 |

Location

90 |

?

91 |
92 | {/* */} 93 | { 96 | if (typeof newValue === 'string') { 97 | setLocation({ 98 | title: newValue, 99 | }); 100 | } else if (newValue && newValue.inputValue) { 101 | // Create a new value from the user input 102 | setLocation({ 103 | title: newValue.inputValue, 104 | }); 105 | } else { 106 | setLocation(newValue); 107 | } 108 | }} 109 | filterOptions={(options, params) => { 110 | const filtered = filter(options, params); 111 | const { inputValue } = params; 112 | // Suggest the creation of a new value 113 | const isExisting = options.some((option) => inputValue === option.title); 114 | if (inputValue !== '' && !isExisting) { 115 | filtered.push({ 116 | //@ts-expect-error 117 | title: inputValue.match(uuidRegex) ? `Block with uuid: ${inputValue}` : `NLP Parse: "${inputValue}"`, 118 | }); 119 | } 120 | 121 | return filtered; 122 | }} 123 | selectOnFocus 124 | clearOnBlur 125 | handleHomeEndKeys 126 | id="locationInput" 127 | options={pagesList} 128 | getOptionLabel={(option) => { 129 | // Value selected with enter, right from the input 130 | if (typeof option === 'string') { 131 | return option; 132 | } 133 | // Add "xxx" option created dynamically 134 | if (option.inputValue) { 135 | return option.inputValue; 136 | } 137 | // Regular option 138 | return option.title; 139 | }} 140 | renderOption={(props, option) =>
  • {option.title}
  • } 141 | sx={{ width: 300 }} 142 | freeSolo 143 | renderInput={(params) => ( 144 | 145 | )} 146 | style={{ backgroundColor: "white", display: "inline-block", padding: "0px", width: "100%" }} 147 | /> 148 |
    149 |
    150 |
    151 |

    Title

    152 |

    ?

    153 |
    154 | 155 |
    156 |
    157 |
    158 |

    Sibling?

    159 |

    ?

    160 |
    161 | 168 |
    169 |
    170 |
    171 |

    Template

    172 |

    ?

    173 |
    174 | 184 |
    185 |
    186 |
    187 |
    188 | 189 |
    190 | ); 191 | }; 192 | 193 | export default InsertionUI; 194 | 195 | interface AutocompleteProps { 196 | inputValue?: string; 197 | title: string; 198 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.0.17 | MIT License | https://tailwindcss.com 3 | *//* 4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 6 | */ 7 | 8 | *, 9 | 10 | ::before, 11 | ::after { 12 | box-sizing: border-box; /* 1 */ 13 | border-width: 0; /* 2 */ 14 | border-style: solid; /* 2 */ 15 | border-color: #e5e7eb; /* 2 */ 16 | 17 | } 18 | 19 | ::before, 20 | ::after { 21 | --tw-content: ''; 22 | } 23 | 24 | /* 25 | 1. Use a consistent sensible line-height in all browsers. 26 | 2. Prevent adjustments of font size after orientation changes in iOS. 27 | 3. Use a more readable tab size. 28 | 4. Use the user's configured `sans` font-family by default. 29 | */ 30 | 31 | html { 32 | line-height: 1.5; /* 1 */ 33 | -webkit-text-size-adjust: 100%; /* 2 */ 34 | -moz-tab-size: 4; /* 3 */ 35 | -o-tab-size: 4; 36 | tab-size: 4; /* 3 */ 37 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */; 38 | --cl-charcoal: #3f3f3f; 39 | --cl-darkjungle: #1e1e1e; 40 | --cl-thunder: #4c4c4c; 41 | 42 | 43 | } 44 | 45 | /* 46 | 1. Remove the margin in all browsers. 47 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 48 | */ 49 | 50 | body { 51 | margin: 0; /* 1 */ 52 | line-height: inherit; /* 2 */ 53 | } 54 | 55 | /* 56 | 1. Add the correct height in Firefox. 57 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 58 | 3. Ensure horizontal rules are visible by default. 59 | */ 60 | 61 | hr { 62 | height: 0; /* 1 */ 63 | color: inherit; /* 2 */ 64 | border-top-width: 1px; /* 3 */ 65 | } 66 | 67 | /* 68 | Add the correct text decoration in Chrome, Edge, and Safari. 69 | */ 70 | 71 | abbr:where([title]) { 72 | -webkit-text-decoration: underline dotted; 73 | text-decoration: underline dotted; 74 | } 75 | 76 | /* 77 | Remove the default font size and weight for headings. 78 | */ 79 | 80 | h1, 81 | h2, 82 | h3, 83 | h4, 84 | h5, 85 | h6 { 86 | font-size: inherit; 87 | font-weight: inherit; 88 | } 89 | 90 | /* 91 | Reset links to optimize for opt-in styling instead of opt-out. 92 | */ 93 | 94 | a { 95 | color: inherit; 96 | text-decoration: inherit; 97 | } 98 | 99 | /* 100 | Add the correct font weight in Edge and Safari. 101 | */ 102 | 103 | b, 104 | strong { 105 | font-weight: bolder; 106 | } 107 | 108 | /* 109 | 1. Use the user's configured `mono` font family by default. 110 | 2. Correct the odd `em` font sizing in all browsers. 111 | */ 112 | 113 | code, 114 | kbd, 115 | samp, 116 | pre { 117 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 118 | font-size: 1em; /* 2 */ 119 | } 120 | 121 | /* 122 | Add the correct font size in all browsers. 123 | */ 124 | 125 | small { 126 | font-size: 80%; 127 | } 128 | 129 | /* 130 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 131 | */ 132 | 133 | sub, 134 | sup { 135 | font-size: 75%; 136 | line-height: 0; 137 | position: relative; 138 | vertical-align: baseline; 139 | } 140 | 141 | sub { 142 | bottom: -0.25em; 143 | } 144 | 145 | sup { 146 | top: -0.5em; 147 | } 148 | 149 | /* 150 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 151 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 152 | 3. Remove gaps between table borders by default. 153 | */ 154 | 155 | table { 156 | text-indent: 0; /* 1 */ 157 | border-color: inherit; /* 2 */ 158 | border-collapse: collapse; /* 3 */ 159 | } 160 | 161 | /* 162 | 1. Change the font styles in all browsers. 163 | 2. Remove the margin in Firefox and Safari. 164 | 3. Remove default padding in all browsers. 165 | */ 166 | 167 | button, 168 | input, 169 | optgroup, 170 | select, 171 | textarea { 172 | font-family: inherit; /* 1 */ 173 | font-size: 100%; /* 1 */ 174 | line-height: inherit; /* 1 */ 175 | color: inherit; /* 1 */ 176 | margin: 0; /* 2 */ 177 | padding: 0; /* 3 */ 178 | } 179 | 180 | /* 181 | Remove the inheritance of text transform in Edge and Firefox. 182 | */ 183 | 184 | button, 185 | select { 186 | text-transform: none; 187 | } 188 | 189 | /* 190 | 1. Correct the inability to style clickable types in iOS and Safari. 191 | 2. Remove default button styles. 192 | */ 193 | 194 | button, 195 | [type='button'], 196 | [type='reset'], 197 | [type='submit'] { 198 | -webkit-appearance: button; /* 1 */ 199 | background-color: transparent; /* 2 */ 200 | background-image: none; /* 2 */ 201 | } 202 | 203 | /* 204 | Use the modern Firefox focus style for all focusable elements. 205 | */ 206 | 207 | :-moz-focusring { 208 | outline: auto; 209 | } 210 | 211 | /* 212 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 213 | */ 214 | 215 | :-moz-ui-invalid { 216 | box-shadow: none; 217 | } 218 | 219 | /* 220 | Add the correct vertical alignment in Chrome and Firefox. 221 | */ 222 | 223 | progress { 224 | vertical-align: baseline; 225 | } 226 | 227 | /* 228 | Correct the cursor style of increment and decrement buttons in Safari. 229 | */ 230 | 231 | ::-webkit-inner-spin-button, 232 | ::-webkit-outer-spin-button { 233 | height: auto; 234 | } 235 | 236 | /* 237 | 1. Correct the odd appearance in Chrome and Safari. 238 | 2. Correct the outline style in Safari. 239 | */ 240 | 241 | [type='search'] { 242 | -webkit-appearance: textfield; /* 1 */ 243 | outline-offset: -2px; /* 2 */ 244 | } 245 | 246 | /* 247 | Remove the inner padding in Chrome and Safari on macOS. 248 | */ 249 | 250 | ::-webkit-search-decoration { 251 | -webkit-appearance: none; 252 | } 253 | 254 | /* 255 | 1. Correct the inability to style clickable types in iOS and Safari. 256 | 2. Change font properties to `inherit` in Safari. 257 | */ 258 | 259 | ::-webkit-file-upload-button { 260 | -webkit-appearance: button; /* 1 */ 261 | font: inherit; /* 2 */ 262 | } 263 | 264 | /* 265 | Add the correct display in Chrome and Safari. 266 | */ 267 | 268 | summary { 269 | display: list-item; 270 | } 271 | 272 | /* 273 | Removes the default spacing and border for appropriate elements. 274 | */ 275 | 276 | blockquote, 277 | dl, 278 | dd, 279 | h1, 280 | h2, 281 | h3, 282 | h4, 283 | h5, 284 | h6, 285 | hr, 286 | figure, 287 | p, 288 | pre { 289 | margin: 0; 290 | } 291 | 292 | fieldset { 293 | margin: 0; 294 | padding: 0; 295 | } 296 | 297 | legend { 298 | padding: 0; 299 | } 300 | 301 | ol, 302 | ul, 303 | menu { 304 | list-style: none; 305 | margin: 0; 306 | padding: 0; 307 | } 308 | 309 | /* 310 | Prevent resizing textareas horizontally by default. 311 | */ 312 | 313 | textarea { 314 | resize: vertical; 315 | } 316 | 317 | /* 318 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 319 | 2. Set the default placeholder color to the user's configured gray 400 color. 320 | */ 321 | 322 | input::-moz-placeholder, textarea::-moz-placeholder { 323 | opacity: 1; /* 1 */ 324 | color: #9ca3af; /* 2 */ 325 | } 326 | 327 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 328 | opacity: 1; /* 1 */ 329 | color: #9ca3af; /* 2 */ 330 | } 331 | 332 | input::placeholder, 333 | textarea::placeholder { 334 | opacity: 1; /* 1 */ 335 | color: #9ca3af; /* 2 */ 336 | } 337 | 338 | /* 339 | Set the default cursor for buttons. 340 | */ 341 | 342 | button, 343 | [role="button"] { 344 | cursor: pointer; 345 | } 346 | 347 | /* 348 | Make sure disabled buttons don't get the pointer cursor. 349 | */ 350 | :disabled { 351 | cursor: default; 352 | } 353 | 354 | /* 355 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 356 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 357 | This can trigger a poorly considered lint error in some tools but is included by design. 358 | */ 359 | 360 | img, 361 | svg, 362 | video, 363 | canvas, 364 | audio, 365 | iframe, 366 | embed, 367 | object { 368 | display: block; /* 1 */ 369 | vertical-align: middle; /* 2 */ 370 | } 371 | 372 | /* 373 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 374 | */ 375 | 376 | img, 377 | video { 378 | max-width: 100%; 379 | height: auto; 380 | } 381 | 382 | /* 383 | Ensure the default browser behavior of the `hidden` attribute. 384 | */ 385 | 386 | [hidden] { 387 | display: none; 388 | } 389 | 390 | *, ::before, ::after { 391 | --tw-translate-x: 0; 392 | --tw-translate-y: 0; 393 | --tw-rotate: 0; 394 | --tw-skew-x: 0; 395 | --tw-skew-y: 0; 396 | --tw-scale-x: 1; 397 | --tw-scale-y: 1; 398 | --tw-pan-x: ; 399 | --tw-pan-y: ; 400 | --tw-pinch-zoom: ; 401 | --tw-scroll-snap-strictness: proximity; 402 | --tw-ordinal: ; 403 | --tw-slashed-zero: ; 404 | --tw-numeric-figure: ; 405 | --tw-numeric-spacing: ; 406 | --tw-numeric-fraction: ; 407 | --tw-ring-inset: ; 408 | --tw-ring-offset-width: 0px; 409 | --tw-ring-offset-color: #fff; 410 | --tw-ring-color: rgb(59 130 246 / 0.5); 411 | --tw-ring-offset-shadow: 0 0 #0000; 412 | --tw-ring-shadow: 0 0 #0000; 413 | --tw-shadow: 0 0 #0000; 414 | --tw-shadow-colored: 0 0 #0000; 415 | --tw-blur: ; 416 | --tw-brightness: ; 417 | --tw-contrast: ; 418 | --tw-grayscale: ; 419 | --tw-hue-rotate: ; 420 | --tw-invert: ; 421 | --tw-saturate: ; 422 | --tw-sepia: ; 423 | --tw-drop-shadow: ; 424 | --tw-backdrop-blur: ; 425 | --tw-backdrop-brightness: ; 426 | --tw-backdrop-contrast: ; 427 | --tw-backdrop-grayscale: ; 428 | --tw-backdrop-hue-rotate: ; 429 | --tw-backdrop-invert: ; 430 | --tw-backdrop-opacity: ; 431 | --tw-backdrop-saturate: ; 432 | --tw-backdrop-sepia: ; 433 | } 434 | .absolute { 435 | position: absolute; 436 | } 437 | .top-10 { 438 | top: 2.5rem; 439 | } 440 | .mr-3 { 441 | margin-right: 0.75rem; 442 | } 443 | .flex { 444 | display: flex; 445 | } 446 | .w-1\/3 { 447 | width: 33.333333%; 448 | } 449 | .w-full { 450 | width: 100%; 451 | } 452 | .appearance-none { 453 | -webkit-appearance: none; 454 | -moz-appearance: none; 455 | appearance: none; 456 | } 457 | .justify-center { 458 | justify-content: center; 459 | } 460 | .rounded-lg { 461 | border-radius: 0.5rem; 462 | } 463 | .border { 464 | border-width: 1px; 465 | } 466 | .border-none { 467 | border-style: none; 468 | } 469 | .border-black { 470 | --tw-border-opacity: 1; 471 | border-color: rgb(0 0 0 / var(--tw-border-opacity)); 472 | } 473 | .bg-white { 474 | --tw-bg-opacity: 1; 475 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 476 | } 477 | .bg-transparent { 478 | background-color: transparent; 479 | } 480 | .p-3 { 481 | padding: 0.75rem; 482 | } 483 | .py-1 { 484 | padding-top: 0.25rem; 485 | padding-bottom: 0.25rem; 486 | } 487 | .px-2 { 488 | padding-left: 0.5rem; 489 | padding-right: 0.5rem; 490 | } 491 | .leading-tight { 492 | line-height: 1.25; 493 | } 494 | .text-gray-700 { 495 | --tw-text-opacity: 1; 496 | color: rgb(55 65 81 / var(--tw-text-opacity)); 497 | } 498 | .focus\:outline-none:focus { 499 | outline: 2px solid transparent; 500 | outline-offset: 2px; 501 | } 502 | 503 | 504 | body {font-family: Arial, Helvetica, sans-serif;} 505 | * {box-sizing: border-box;} 506 | 507 | .form-inline { 508 | display: flex; 509 | flex-flow: row wrap; 510 | align-items: center; 511 | } 512 | 513 | .form-inline label { 514 | margin: 5px 10px 5px 0; 515 | } 516 | 517 | .form-inline input { 518 | vertical-align: middle; 519 | margin: 5px 10px 5px 0; 520 | padding: 10px; 521 | background-color: #fff; 522 | border: 1px solid #ddd; 523 | } 524 | 525 | .button-section { 526 | margin: 3%; 527 | } 528 | .button { 529 | padding: 10px 20px; 530 | /* background-color: dodgerblue; */ 531 | border: 1px solid #ddd; 532 | color: white; 533 | cursor: pointer; 534 | border-radius: 10px; 535 | } 536 | 537 | .button:hover { 538 | background-color: rgb(0, 0, 0); 539 | } 540 | 541 | .remove { 542 | background-color: rgb(192, 53, 53); 543 | } 544 | .remove:hover { 545 | background-color: rgb(187, 43, 43); 546 | } 547 | .overlay { 548 | position: fixed; 549 | top: 0; 550 | left: 0; 551 | width: 100vw; 552 | height: 100vh; 553 | background: hsla(0,0%,10%,.5); 554 | z-index: 9; 555 | } 556 | .smartblock-popup { 557 | padding: 1em; 558 | background: rgb(40, 157, 165); 559 | z-index: 9; 560 | border-radius: 10px; 561 | 562 | /* -ms-transform: translate(-50%, -50%); 563 | transform: translate(-50%, -50%); */ 564 | } 565 | .popup-content { 566 | z-index: 10; 567 | } 568 | 569 | .centered-element { 570 | margin: 0; 571 | position: absolute; 572 | top: 30%; 573 | transform: translateY(-50%); 574 | } 575 | 576 | .grid-container { 577 | display: grid; 578 | grid-template-columns: 1fr 1fr; 579 | grid-gap: 20px; 580 | padding-top: 10px; 581 | } 582 | .label-class { 583 | color: whitesmoke; 584 | font-family: system-ui; 585 | padding-bottom: 10px; 586 | font-size: larger;} 587 | .smartblock-inserter { 588 | padding: 1em; 589 | /* background: rgb(40, 157, 165); */ 590 | z-index: 9; 591 | border-radius: 15px; 592 | top: 30%; 593 | width: 50%; 594 | display: flex; 595 | transform: translate(0, 100px); 596 | flex-direction: column; 597 | color: #ffffff; 598 | align-items: center; 599 | background-color: var(--cl-darkjungle)!important; 600 | border-top: 1px solid var(--cl-charcoal); 601 | border-right: 1px solid var(--cl-charcoal); 602 | border-bottom: 1px solid var(--cl-charcoal); 603 | border-left: 1px solid var(--cl-charcoal); 604 | font-size: 1.2em; 605 | font-family: system-ui; 606 | padding: 0.5em; 607 | border-radius: 5px; 608 | outline: none; 609 | /* box-shadow: 0 0 0 0 transparent; */ 610 | /* transition: box-shadow 0.3s ease-in-out; */ 611 | } 612 | .cp__palette-input{ 613 | width: 100%; 614 | height: 100%; 615 | border: none; 616 | background: transparent; 617 | color: #ffffff !important; 618 | font-size: 1.2em; 619 | font-family: system-ui; 620 | padding: 0.5em; 621 | border-radius: 5px; 622 | outline: none; 623 | box-shadow: 0 0 0 0 transparent; 624 | transition: box-shadow 0.3s ease-in-out; 625 | } 626 | .searchItem{ 627 | color: #ffffff 628 | } 629 | .css-1na68kg .MuiOutlinedInput-root { 630 | /* padding: 0px; */ 631 | } 632 | 633 | select { 634 | color: black !important; 635 | } 636 | 637 | input { 638 | color: black !important; 639 | } --------------------------------------------------------------------------------