├── .eslintrc.cjs ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── chrome.png ├── firefox.png └── linkwarden-extension.png ├── build.sh ├── chromium └── manifest.json ├── components.json ├── firefox └── manifest.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public ├── 128.png ├── 16.png ├── 32.png └── 48.png ├── src ├── @ │ ├── components │ │ ├── BookmarkForm.tsx │ │ ├── Container.tsx │ │ ├── Modal.tsx │ │ ├── ModeToggle.tsx │ │ ├── OptionsForm.tsx │ │ ├── TagInput.tsx │ │ ├── ThemeProvider.tsx │ │ ├── WholeContainer.tsx │ │ └── ui │ │ │ ├── Button.tsx │ │ │ ├── CheckBox.tsx │ │ │ ├── Command.tsx │ │ │ ├── Dialog.tsx │ │ │ ├── DropDownMenu.tsx │ │ │ ├── Form.tsx │ │ │ ├── Input.tsx │ │ │ ├── Label.tsx │ │ │ ├── Popover.tsx │ │ │ ├── Select.tsx │ │ │ ├── Separator.tsx │ │ │ ├── Textarea.tsx │ │ │ ├── Toast.tsx │ │ │ └── Toaster.tsx │ └── lib │ │ ├── actions │ │ ├── collections.ts │ │ ├── links.ts │ │ └── tags.ts │ │ ├── api.ts │ │ ├── auth │ │ └── auth.ts │ │ ├── cache.ts │ │ ├── config.ts │ │ ├── screenshot.ts │ │ ├── utils.ts │ │ └── validators │ │ ├── bookmarkForm.ts │ │ ├── config.ts │ │ └── optionsForm.ts ├── hooks │ └── use-toast.ts ├── pages │ ├── Background │ │ └── index.ts │ ├── Options │ │ ├── App.tsx │ │ ├── Options.tsx │ │ └── options.html │ └── Popup │ │ ├── App.tsx │ │ ├── index.css │ │ └── main.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | dist.crx 15 | dist.pem 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | linkwarden.zip 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "requirePragma": false, 5 | "arrowParens": "always", 6 | "tabWidth": 2, 7 | "endOfLine": "lf", 8 | "semi": true 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Linkwarden 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linkwarden Browser Extension 2 | 3 | The Official Browser Extension for [Linkwarden](https://github.com/linkwarden/linkwarden). 4 | 5 | ## Features 6 | 7 | - Add and organize new links to Linkwarden with a single click. 8 | - Upload screenshots of the current page to Linkwarden. 9 | - Save all tabs in the current window to Linkwarden. 10 | - Sign in using API key or Username/Password. 11 | 12 | ![Image](/assets/linkwarden-extension.png) 13 | 14 | ## Installation 15 | 16 | You can get the browser extension from both the Chrome Web Store and Firefox Add-ons: 17 | 18 | Chrome Web Store 19 | Firefox Add-ons 20 | 21 | ## Issues and Feature Requests 22 | 23 | We decided to keep the issues and feature requests in the main repository to keep everything in one place. Please report any issues or feature requests from the official repository, starting the title with "[Browser Extension]" [here](https://github.com/linkwarden/linkwarden/issues/new/choose). 24 | 25 | ## Build From Source 26 | 27 | ### Requirements 28 | 29 | - LTS NodeJS 18.x.x 30 | - NPM Version 9.x.x 31 | - Bash 32 | - Git 33 | 34 | ### Step 1: Clone this repo 35 | 36 | Clone this repository by running the following in your terminal: 37 | 38 | ``` 39 | git clone https://github.com/linkwarden/browser-extension.git 40 | ``` 41 | 42 | ### Step 2: Build 43 | 44 | Head to the generated folder: 45 | 46 | ``` 47 | cd browser-extension 48 | ``` 49 | 50 | And run: 51 | 52 | ``` 53 | chmod +x ./build.sh && ./build.sh 54 | ``` 55 | 56 | After the above command, use the `/dist` folder as an unpacked extension in your browser. 57 | -------------------------------------------------------------------------------- /assets/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkwarden/browser-extension/2810d84d20e33eda9179909af09ea29885ac7dda/assets/chrome.png -------------------------------------------------------------------------------- /assets/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkwarden/browser-extension/2810d84d20e33eda9179909af09ea29885ac7dda/assets/firefox.png -------------------------------------------------------------------------------- /assets/linkwarden-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkwarden/browser-extension/2810d84d20e33eda9179909af09ea29885ac7dda/assets/linkwarden-extension.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Install deps 4 | npm install 5 | 6 | # Build 7 | npm run build 8 | 9 | # Check if --firefox argument was passed 10 | if [ "$1" = "--firefox" ]; then 11 | # Copy to firefox/manifest.json 12 | echo "Built for Firefox..." 13 | cp firefox/manifest.json dist/manifest.json 14 | else 15 | # Copy to dist/manifest.json 16 | echo "Built for Chromium..." 17 | cp chromium/manifest.json dist/manifest.json 18 | fi 19 | 20 | # Done (for now...) 21 | echo "Done! ✅" 22 | -------------------------------------------------------------------------------- /chromium/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Linkwarden", 4 | "description": "The browser extension for Linkwarden.", 5 | "homepage_url": "https://linkwarden.app/", 6 | "version": "1.3.3", 7 | "action": { 8 | "default_popup": "./index.html", 9 | "default_icon": { 10 | "16": "16.png", 11 | "32": "32.png", 12 | "48": "48.png", 13 | "128": "128.png" 14 | }, 15 | "default_title": "Linkwarden" 16 | }, 17 | "options_ui": { 18 | "page": "./src/pages/Options/options.html", 19 | "browser_style": false 20 | }, 21 | "icons": { 22 | "16": "16.png", 23 | "32": "32.png", 24 | "48": "48.png", 25 | "128": "128.png" 26 | }, 27 | "permissions": [ 28 | "storage", 29 | "scripting", 30 | "activeTab", 31 | "tabs", 32 | "bookmarks", 33 | "commands", 34 | "contextMenus" 35 | ], 36 | "background": { 37 | "service_worker": "./background.js", 38 | "type": "module" 39 | }, 40 | "omnibox": { 41 | "keyword": "lk" 42 | }, 43 | "host_permissions": ["*://*/*"], 44 | "commands": { 45 | "_execute_action": { 46 | "suggested_key": { 47 | "default": "Ctrl+Shift+F", 48 | "mac": "Command+Shift+Y" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "src/@/components", 14 | "utils": "src/@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Linkwarden", 4 | "description": "The browser extension for Linkwarden.", 5 | "homepage_url": "https://linkwarden.app/", 6 | "version": "1.3.3", 7 | "browser_action": { 8 | "default_popup": "./index.html", 9 | "default_icon": { 10 | "16": "./16.png", 11 | "32": "./32.png", 12 | "48": "./48.png", 13 | "128": "./128.png" 14 | }, 15 | "default_title": "Linkwarden" 16 | }, 17 | "options_ui": { 18 | "page": "./src/pages/Options/options.html", 19 | "browser_style": false 20 | }, 21 | "icons": { 22 | "16": "./16.png", 23 | "32": "./32.png", 24 | "48": "./48.png", 25 | "128": "./128.png" 26 | }, 27 | "permissions": [ 28 | "storage", 29 | "activeTab", 30 | "tabs", 31 | "bookmarks", 32 | "contextMenus", 33 | "", 34 | "http://*/*", 35 | "https://*/*" 36 | ], 37 | "commands": { 38 | "_execute_browser_action": { 39 | "suggested_key": { 40 | "default": "Ctrl+Shift+F", 41 | "mac": "Command+Shift+K" 42 | } 43 | } 44 | }, 45 | "omnibox": { 46 | "keyword": "lk" 47 | }, 48 | "background": { 49 | "scripts": ["background.js"], 50 | "persistent": false, 51 | "type": "module" 52 | }, 53 | "browser_specific_settings": { 54 | "gecko": { 55 | "id": "jordanlinkwarden@gmail.com", 56 | "strict_min_version": "109.0" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Linkwarden Extension 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkwarden-extension", 3 | "private": true, 4 | "version": "0.0.1", 5 | "author": "Jordan Higuera Higuera ", 6 | "type": "module", 7 | "license": "MIT", 8 | "description": "Linkwarden browser extension", 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "tsc && vite build", 12 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 13 | "preview": "vite preview" 14 | }, 15 | "dependencies": { 16 | "@hookform/resolvers": "^3.2.0", 17 | "@radix-ui/react-checkbox": "^1.0.4", 18 | "@radix-ui/react-dialog": "^1.0.4", 19 | "@radix-ui/react-dropdown-menu": "^2.0.6", 20 | "@radix-ui/react-icons": "^1.3.0", 21 | "@radix-ui/react-label": "^2.0.2", 22 | "@radix-ui/react-popover": "^1.0.6", 23 | "@radix-ui/react-select": "^1.2.2", 24 | "@radix-ui/react-separator": "^1.0.3", 25 | "@radix-ui/react-slot": "^1.0.2", 26 | "@radix-ui/react-toast": "^1.1.4", 27 | "@tanstack/react-query": "^4.32.6", 28 | "@types/chrome": "^0.0.243", 29 | "axios": "^1.4.0", 30 | "class-variance-authority": "^0.7.0", 31 | "clsx": "^2.0.0", 32 | "cmdk": "^0.2.0", 33 | "lucide-react": "^0.264.0", 34 | "query-string": "^8.1.0", 35 | "react": "^18.2.0", 36 | "react-dom": "^18.2.0", 37 | "react-hook-form": "^7.45.4", 38 | "tailwind-merge": "^1.14.0", 39 | "tailwindcss-animate": "^1.0.6", 40 | "zod": "^3.21.4" 41 | }, 42 | "devDependencies": { 43 | "@types/firefox-webext-browser": "^120.0.0", 44 | "@types/node": "^20.4.8", 45 | "@types/react": "^18.2.15", 46 | "@types/react-dom": "^18.2.7", 47 | "@types/webextension-polyfill": "^0.12.1", 48 | "@typescript-eslint/eslint-plugin": "^6.0.0", 49 | "@typescript-eslint/parser": "^6.0.0", 50 | "@vitejs/plugin-react": "^4.0.3", 51 | "autoprefixer": "^10.4.14", 52 | "eslint": "^8.45.0", 53 | "eslint-plugin-react-hooks": "^4.6.0", 54 | "eslint-plugin-react-refresh": "^0.4.3", 55 | "postcss": "^8.4.27", 56 | "tailwindcss": "^3.3.3", 57 | "typescript": "^5.0.2", 58 | "vite": "^4.4.5", 59 | "webextension-polyfill": "^0.12.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /public/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkwarden/browser-extension/2810d84d20e33eda9179909af09ea29885ac7dda/public/128.png -------------------------------------------------------------------------------- /public/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkwarden/browser-extension/2810d84d20e33eda9179909af09ea29885ac7dda/public/16.png -------------------------------------------------------------------------------- /public/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkwarden/browser-extension/2810d84d20e33eda9179909af09ea29885ac7dda/public/32.png -------------------------------------------------------------------------------- /public/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkwarden/browser-extension/2810d84d20e33eda9179909af09ea29885ac7dda/public/48.png -------------------------------------------------------------------------------- /src/@/components/BookmarkForm.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form'; 2 | import { 3 | bookmarkFormSchema, 4 | bookmarkFormValues, 5 | } from '../lib/validators/bookmarkForm.ts'; 6 | import { zodResolver } from '@hookform/resolvers/zod'; 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormLabel, 13 | FormMessage, 14 | } from './ui/Form.tsx'; 15 | import { Input } from './ui/Input.tsx'; 16 | import { Button } from './ui/Button.tsx'; 17 | import { TagInput } from './TagInput.tsx'; 18 | import { Textarea } from './ui/Textarea.tsx'; 19 | import { checkDuplicatedItem, getCurrentTabInfo } from '../lib/utils.ts'; 20 | import { useEffect, useState } from 'react'; 21 | import { useMutation, useQuery } from '@tanstack/react-query'; 22 | import { getConfig, isConfigured } from '../lib/config.ts'; 23 | import { postLink } from '../lib/actions/links.ts'; 24 | import { AxiosError } from 'axios'; 25 | import { toast } from '../../hooks/use-toast.ts'; 26 | import { Toaster } from './ui/Toaster.tsx'; 27 | import { getCollections } from '../lib/actions/collections.ts'; 28 | import { getTags } from '../lib/actions/tags.ts'; 29 | import { X } from 'lucide-react'; 30 | import { Popover, PopoverContent, PopoverTrigger } from './ui/Popover.tsx'; 31 | import { CaretSortIcon } from '@radix-ui/react-icons'; 32 | import { 33 | Command, 34 | CommandEmpty, 35 | CommandGroup, 36 | CommandInput, 37 | CommandItem, 38 | } from './ui/Command.tsx'; 39 | import { saveLinksInCache } from '../lib/cache.ts'; 40 | import { Checkbox } from './ui/CheckBox.tsx'; 41 | import { Label } from './ui/Label.tsx'; 42 | 43 | let configured = false; 44 | let duplicated = false; 45 | const BookmarkForm = () => { 46 | const [openOptions, setOpenOptions] = useState(false); 47 | const [openCollections, setOpenCollections] = useState(false); 48 | const [uploadImage, setUploadImage] = useState(false); 49 | const [state, setState] = useState<'capturing' | 'uploading' | null>(null); 50 | 51 | const handleCheckedChange = (s: boolean | 'indeterminate') => { 52 | if (s === 'indeterminate') return; 53 | setUploadImage(s); 54 | form.setValue('image', s ? 'png' : undefined); 55 | }; 56 | 57 | const form = useForm({ 58 | resolver: zodResolver(bookmarkFormSchema), 59 | defaultValues: { 60 | url: '', 61 | name: '', 62 | collection: { 63 | name: 'Unorganized', 64 | }, 65 | tags: [], 66 | description: '', 67 | image: undefined, 68 | }, 69 | }); 70 | 71 | const { mutate: onSubmit, isLoading } = useMutation({ 72 | mutationFn: async (values: bookmarkFormValues) => { 73 | const config = await getConfig(); 74 | 75 | await postLink( 76 | config.baseUrl, 77 | uploadImage, 78 | values, 79 | setState, 80 | config.apiKey 81 | ); 82 | 83 | return; 84 | }, 85 | onError: (error) => { 86 | console.error(error); 87 | if (error instanceof AxiosError) { 88 | toast({ 89 | title: 'Error', 90 | description: 91 | error.response?.data.response || 92 | 'There was an error while trying to save the link. Please try again.', 93 | variant: 'destructive', 94 | }); 95 | } else { 96 | toast({ 97 | title: 'Error', 98 | description: 99 | 'There was an error while trying to save the link. Please try again.', 100 | variant: 'destructive', 101 | }); 102 | } 103 | return; 104 | }, 105 | onSuccess: () => { 106 | setTimeout(() => { 107 | window.close(); 108 | // I want to show some confirmation before it's closed... 109 | }, 3500); 110 | toast({ 111 | title: 'Success', 112 | description: 'Link saved successfully!', 113 | }); 114 | }, 115 | }); 116 | 117 | const { handleSubmit, control } = form; 118 | 119 | useEffect(() => { 120 | getCurrentTabInfo().then(({ url, title }) => { 121 | getConfig().then((config) => { 122 | form.setValue('url', url ? url : ''); 123 | form.setValue('name', title ? title : ''); 124 | form.setValue('collection', { 125 | name: config.defaultCollection, 126 | }); 127 | }); 128 | }); 129 | const getConfigUse = async () => { 130 | configured = await isConfigured(); 131 | duplicated = await checkDuplicatedItem(); 132 | }; 133 | getConfigUse(); 134 | }, [form]); 135 | 136 | useEffect(() => { 137 | const syncBookmarks = async () => { 138 | try { 139 | const { syncBookmarks, baseUrl, defaultCollection } = await getConfig(); 140 | form.setValue('collection', { 141 | name: defaultCollection, 142 | }); 143 | if (!syncBookmarks) { 144 | return; 145 | } 146 | if (await isConfigured()) { 147 | await saveLinksInCache(baseUrl); 148 | //await syncLocalBookmarks(baseUrl); 149 | } 150 | } catch (error) { 151 | console.error(error); 152 | } 153 | }; 154 | syncBookmarks(); 155 | }, [form]); 156 | 157 | const { 158 | isLoading: loadingCollections, 159 | data: collections, 160 | error: collectionError, 161 | } = useQuery({ 162 | queryKey: ['collections'], 163 | queryFn: async () => { 164 | const config = await getConfig(); 165 | 166 | const response = await getCollections(config.baseUrl, config.apiKey); 167 | 168 | return response.data.response.sort((a, b) => { 169 | return a.pathname.localeCompare(b.pathname); 170 | }); 171 | }, 172 | enabled: configured, 173 | }); 174 | 175 | const { 176 | isLoading: loadingTags, 177 | data: tags, 178 | error: tagsError, 179 | } = useQuery({ 180 | queryKey: ['tags'], 181 | queryFn: async () => { 182 | const config = await getConfig(); 183 | 184 | const response = await getTags(config.baseUrl, config.apiKey); 185 | 186 | return response.data.response.sort((a, b) => { 187 | return a.name.localeCompare(b.name); 188 | }); 189 | }, 190 | enabled: configured, 191 | }); 192 | 193 | return ( 194 |
195 |
196 | onSubmit(e))} className="py-1"> 197 | {collectionError ? ( 198 |

199 | There was an error, please make sure the website is available. 200 |

201 | ) : null} 202 | ( 206 | 207 | Collection 208 |
209 | 213 | 214 | 215 | 233 | 234 | 235 | 236 | {!openOptions && openCollections ? ( 237 |
244 | 250 | 251 | 255 | No Collection found. 256 | {Array.isArray(collections) && ( 257 | 258 | {isLoading ? ( 259 | { 263 | form.setValue('collection', { 264 | name: 'Unorganized', 265 | }); 266 | setOpenCollections(false); 267 | }} 268 | > 269 | Unorganized 270 | 271 | ) : ( 272 | collections?.map( 273 | (collection: { 274 | name: string; 275 | id: number; 276 | ownerId: number; 277 | pathname: string; 278 | }) => ( 279 | { 284 | form.setValue('collection', { 285 | ownerId: collection.ownerId, 286 | id: collection.id, 287 | name: collection.name, 288 | }); 289 | setOpenCollections(false); 290 | }} 291 | > 292 |

{collection.name}

293 |

294 | {collection.pathname} 295 |

296 |
297 | ) 298 | ) 299 | )} 300 |
301 | )} 302 |
303 |
304 | ) : openOptions && openCollections ? ( 305 | 308 | 309 | 313 | No Collection found. 314 | {Array.isArray(collections) && ( 315 | 316 | {isLoading ? ( 317 | { 321 | form.setValue('collection', { 322 | name: 'Unorganized', 323 | }); 324 | setOpenCollections(false); 325 | }} 326 | > 327 | Unorganized 328 | 329 | ) : ( 330 | collections?.map( 331 | (collection: { 332 | name: string; 333 | id: number; 334 | ownerId: number; 335 | }) => ( 336 | { 340 | form.setValue('collection', { 341 | ownerId: collection.ownerId, 342 | id: collection.id, 343 | name: collection.name, 344 | }); 345 | setOpenCollections(false); 346 | }} 347 | > 348 | {collection.name} 349 | 350 | ) 351 | ) 352 | )} 353 | 354 | )} 355 | 356 | 357 | ) : undefined} 358 |
359 |
360 | 361 |
362 | )} 363 | /> 364 | {openOptions && ( 365 |
366 | {tagsError ?

There was an error...

: null} 367 | ( 371 | 372 | Tags 373 | {loadingTags ? ( 374 | 379 | ) : tagsError ? ( 380 | 385 | ) : ( 386 | 391 | )} 392 | 393 | 394 | )} 395 | /> 396 | ( 400 | 401 | Name 402 | 403 | 404 | 405 | 406 | 407 | )} 408 | /> 409 | ( 413 | 414 | Description 415 | 416 |