diff --git a/tools/plugins/tags/README.md b/tools/plugins/tags/README.md new file mode 100644 index 0000000..4877115 --- /dev/null +++ b/tools/plugins/tags/README.md @@ -0,0 +1,81 @@ +# Tags Plugin + +A multi-select, searchable tag picker for Adobe Document Authoring (DA) Edge Delivery Services and Universal Editor (UE) environments. + +## Features + +- **Fetches tags** from a JSON file (`/docs/library/tagging.json`) in your DA repo. +- **Searchable**: Quickly filter tags by value, key, or comments. +- **Multi-select**: Select multiple tags using checkboxes. +- **Bulk actions**: Select all, deselect all, and send all selected tags at once. +- **Accessible UI**: Keyboard and screen-reader friendly. +- **Modern, responsive design**: Clean, sticky action bar and mobile-friendly layout. + +## Usage + +1. **Add the plugin to your DA tools directory** (already present as `tools/plugins/tags/`). +2. **Ensure your DA repo contains a `docs/library/tagging.json` file** with the following structure: + + ```json + { + "data": [ + { + "key": "tag-key-1", + "value": "Tag Value 1", + "comments": "Optional description / unsed in UI" + }, + { + "key": "tag-key-2", + "value": "Tag Value 2" + } + ], + "limit": 100 + } + ``` + + - `key`: The value sent to the document when selected. + - `value`: The label shown in the UI. + - `comments`: (Optional) Additional info shown in the UI. + +3. **Open `tools/tags/tags.html` in the DA/UE environment**. + - The plugin will automatically fetch and display tags from your repo. + - Use the search box to filter tags. + - Select tags using checkboxes. + - Use the action bar at the bottom to select all, deselect all, or send selected tags. + - Click "Send Selected" to insert the selected tag keys (comma-separated) into your document. + +## File Overview + +- `tags.html` – Minimal HTML shell, loads the plugin and styles. +- `tags.js` – Main plugin logic (fetch, render, search, select, send). +- `tags.css` – All UI styles (customizable). + +## Integration + +- **DA SDK**: Uses the DA App SDK (`https://da.live/nx/utils/sdk.js`) for context, fetch, and document actions. +- **No build step required**: All files are plain JS/CSS/HTML. +- **No external dependencies**: Only the DA SDK is required. + +### Configuration + +> Site _CONFIG_ > _library_ + +| title | path | icon | ref | format | experience | +| ------- | ----------------------- | -------------------------------------------------------------------- | --- | --- | -------- | +| `Tags` | `/tools/plugins/tags/tags.html` | `https://main--{site}--{org}.aem.page/tools/plugins/tags/classification.svg` | | | `dialog` | + +## Customization + +- **Styling**: Edit `tags.css` to change the look and feel. +- **Data source**: Change the fetch URL in `tags.js` if your tagging data is elsewhere. +- **Button text/labels**: Edit in `tags.js` for localization or branding. + +## Development + +- Lint with `npm run lint`. +- All code is ES6+ and uses modern best practices. +- No console logging except for errors. + +## License + +[MIT](../../LICENSE) (or your project’s license) diff --git a/tools/plugins/tags/classification.svg b/tools/plugins/tags/classification.svg new file mode 100644 index 0000000..befa154 --- /dev/null +++ b/tools/plugins/tags/classification.svg @@ -0,0 +1,2 @@ + +classification \ No newline at end of file diff --git a/tools/plugins/tags/tags.css b/tools/plugins/tags/tags.css new file mode 100644 index 0000000..9f0a845 --- /dev/null +++ b/tools/plugins/tags/tags.css @@ -0,0 +1,187 @@ +/* Tags Tool Styles */ + +/* Main container */ +.tags-container { + height: 100vh; + display: flex; + flex-direction: column; + font-family: Arial, sans-serif; + max-width: 600px; + margin: 0 auto; +} + +/* Header */ +.tags-header { + margin: 5px 20px 5px 20px; + text-align: center; + flex-shrink: 0; + font-style: oblique; +} + +/* Error message */ +.error-message { + color: #721c24; + background-color: #f8d7da; + padding: 12px; + border-radius: 6px; + border: 1px solid #f5c6cb; + margin-bottom: 20px; +} + +/* Warning message */ +.warning-message { + color: #856404; + background-color: #fff3cd; + padding: 12px; + border-radius: 6px; + border: 1px solid #ffeaa7; +} + +/* Search container */ +.search-container { + margin: 0 20px 20px 20px; + flex-shrink: 0; +} + +/* Search input */ +.search-input { + width: 100%; + padding: 12px 16px; + border: 2px solid #dee2e6; + border-radius: 6px; + font-size: 16px; + box-sizing: border-box; + transition: border-color 0.2s; +} + +.search-input:focus { + border-color: #007bff; +} + +.search-input:blur { + border-color: #dee2e6; +} + +/* Results container */ +.results-container { + flex: 1; + overflow-y: auto; + border: 1px solid #dee2e6; + border-radius: 6px; + margin: 0 20px; +} + +/* No results message */ +.no-results { + padding: 20px; + text-align: center; + color: #6c757d; +} + +/* Tag item */ +.tag-item { + padding: 12px 16px; + border-bottom: 1px solid #f8f9fa; + transition: background-color 0.2s; + display: flex; + align-items: center; + gap: 12px; +} + +.tag-item:hover { + background-color: #f8f9fa; +} + +/* Checkbox */ +.tag-checkbox { + margin: 0; + cursor: pointer; +} + +/* Tag info */ +.tag-info { + flex: 1; + cursor: pointer; +} + +.tag-value { + font-weight: bold; + color: #212529; + margin-bottom: 4px; +} + +.tag-key { + font-size: 12px; + color: #6c757d; +} + +.tag-comments { + font-size: 12px; + color: #6c757d; + font-style: italic; + margin-top: 2px; +} + +/* Action buttons container */ +.action-container { + display: flex; + gap: 10px; + margin: 20px; + justify-content: center; + flex-shrink: 0; + padding: 20px 0; + border-top: 1px solid #dee2e6; + background-color: white; +} + +/* Button base styles */ +.btn { + padding: 10px 20px; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s; +} + +/* Secondary button */ +.btn-secondary { + background-color: #6c757d; +} + +.btn-secondary:hover { + background-color: #5a6268; +} + +/* Primary button */ +.btn-primary { + background-color: #007bff; + font-weight: bold; +} + +.btn-primary:hover { + background-color: #0056b3; +} + +/* Disabled button */ +.btn:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +/* Error button state */ +.btn-error { + background-color: #dc3545; +} + +/* Summary */ +.summary { + background-color: #f8f9fa; + padding: 15px; + border-radius: 6px; + border: 1px solid #dee2e6; + font-size: 14px; + margin: 0 20px 20px 20px; + flex-shrink: 0; +} \ No newline at end of file diff --git a/tools/plugins/tags/tags.html b/tools/plugins/tags/tags.html new file mode 100644 index 0000000..a65bc60 --- /dev/null +++ b/tools/plugins/tags/tags.html @@ -0,0 +1,15 @@ + + + + Tag List + + + + + + + + + + + \ No newline at end of file diff --git a/tools/plugins/tags/tags.js b/tools/plugins/tags/tags.js new file mode 100644 index 0000000..7646212 --- /dev/null +++ b/tools/plugins/tags/tags.js @@ -0,0 +1,282 @@ +// eslint-disable-next-line import/no-unresolved +import DA_SDK from 'https://da.live/nx/utils/sdk.js'; +// eslint-disable-next-line import/no-unresolved +import { DA_ORIGIN } from 'https://da.live/nx/public/utils/constants.js'; + +/** + * Fetches the tagging JSON data from the specified endpoint + * @param {string} token - Authentication token + * @param {Object} actions - DA actions object + * @param {string} org - Organization name + * @param {string} repo - Repository name + * @returns {Promise} The tagging JSON data + */ +async function fetchTaggingData(token, actions, org, repo) { + try { + const taggingUrl = `${DA_ORIGIN}/source/${org}/${repo}/docs/library/tagging.json`; + + const response = await actions.daFetch(taggingUrl); + + if (!response.ok) { + // eslint-disable-next-line no-console + console.error(`Failed to fetch tagging data: ${response.status} ${response.statusText}`); + return null; + } + + const taggingData = await response.json(); + return taggingData; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching tagging data:', error); + return null; + } +} + +/** + * Displays the tagging data as a multi-selectable searchable interface + * @param {Object} taggingData - The tagging JSON data + * @param {Object} actions - DA actions object + */ +function displayTaggingData(taggingData, actions) { + if (!taggingData) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-message'; + errorDiv.textContent = '❌ Failed to load tagging data'; + document.body.appendChild(errorDiv); + return; + } + + // Create container for tagging data + const container = document.createElement('div'); + container.className = 'tags-container'; + + // Create header + const header = document.createElement('h2'); + header.textContent = 'TAGGER'; + header.className = 'tags-header'; + container.appendChild(header); + + // Check if data array exists + if (!taggingData.data || !Array.isArray(taggingData.data)) { + const noDataDiv = document.createElement('div'); + noDataDiv.className = 'warning-message'; + noDataDiv.textContent = '⚠️ No tagging data found'; + container.appendChild(noDataDiv); + document.body.appendChild(container); + return; + } + + // Create search container + const searchContainer = document.createElement('div'); + searchContainer.className = 'search-container'; + + // Create search input + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = 'Search tags...'; + searchInput.className = 'search-input'; + + searchContainer.appendChild(searchInput); + container.appendChild(searchContainer); + + // Create results container + const resultsContainer = document.createElement('div'); + resultsContainer.className = 'results-container'; + container.appendChild(resultsContainer); + + // Track selected tags + const selectedTags = new Set(); + + // Create select all button + const selectAllBtn = document.createElement('button'); + selectAllBtn.textContent = 'Select All'; + selectAllBtn.className = 'btn btn-secondary'; + + // Create deselect all button + const deselectAllBtn = document.createElement('button'); + deselectAllBtn.textContent = 'Deselect All'; + deselectAllBtn.className = 'btn btn-secondary'; + + // Create send selected button + const sendSelectedBtn = document.createElement('button'); + sendSelectedBtn.textContent = 'Send Selected (0)'; + sendSelectedBtn.className = 'btn btn-primary'; + + // Function to update send button text and state + function updateSendButton() { + const count = selectedTags.size; + sendSelectedBtn.textContent = `Send Selected (${count})`; + + if (count > 0) { + sendSelectedBtn.className = 'btn btn-primary'; + sendSelectedBtn.disabled = false; + } else { + sendSelectedBtn.className = 'btn btn-secondary'; + sendSelectedBtn.disabled = true; + } + } + + // Create tag list function + function renderTagList(filteredData) { + resultsContainer.innerHTML = ''; + + if (filteredData.length === 0) { + const noResultsDiv = document.createElement('div'); + noResultsDiv.className = 'no-results'; + noResultsDiv.textContent = 'No tags found matching your search'; + resultsContainer.appendChild(noResultsDiv); + return; + } + + filteredData.forEach((item) => { + if (item.value) { + const tagItem = document.createElement('div'); + tagItem.className = 'tag-item'; + + // Create checkbox + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'tag-checkbox'; + checkbox.checked = selectedTags.has(item.key); + + checkbox.addEventListener('change', (e) => { + if (e.target.checked) { + selectedTags.add(item.key); + } else { + selectedTags.delete(item.key); + } + updateSendButton(); + }); + + // Create tag info + const tagInfo = document.createElement('div'); + tagInfo.className = 'tag-info'; + + const tagValue = document.createElement('div'); + tagValue.textContent = item.value; + tagValue.className = 'tag-value'; + + const tagKey = document.createElement('div'); + tagKey.textContent = `Key: ${item.key}`; + tagKey.className = 'tag-key'; + + if (item.comments) { + const tagComments = document.createElement('div'); + tagComments.textContent = item.comments; + tagComments.className = 'tag-comments'; + tagInfo.appendChild(tagComments); + } + + tagInfo.appendChild(tagValue); + tagInfo.appendChild(tagKey); + + // Make entire tag info clickable to toggle checkbox + tagInfo.addEventListener('click', () => { + checkbox.checked = !checkbox.checked; + checkbox.dispatchEvent(new Event('change')); + }); + + tagItem.appendChild(checkbox); + tagItem.appendChild(tagInfo); + + resultsContainer.appendChild(tagItem); + } + }); + } + + // Add search functionality + searchInput.addEventListener('input', (e) => { + const searchTerm = e.target.value.toLowerCase(); + const filteredData = taggingData.data.filter((item) => item.value && ( + item.value.toLowerCase().includes(searchTerm) + || item.key.toLowerCase().includes(searchTerm) + || (item.comments && item.comments.toLowerCase().includes(searchTerm)) + )); + renderTagList(filteredData); + }); + + // Create action buttons container + const actionContainer = document.createElement('div'); + actionContainer.className = 'action-container'; + container.appendChild(actionContainer); + + selectAllBtn.addEventListener('click', () => { + const visibleItems = taggingData.data.filter((item) => item.value); + visibleItems.forEach((item) => selectedTags.add(item.key)); + renderTagList(taggingData.data.filter((item) => item.value)); + updateSendButton(); + }); + + deselectAllBtn.addEventListener('click', () => { + selectedTags.clear(); + renderTagList(taggingData.data.filter((item) => item.value)); + updateSendButton(); + }); + + sendSelectedBtn.addEventListener('click', async () => { + if (selectedTags.size === 0) return; + + try { + // Send all selected tags + const selectedTagsArray = Array.from(selectedTags); + const tagsText = selectedTagsArray.join(', '); + + await actions.sendText(tagsText); + await actions.closeLibrary(); + + // eslint-disable-next-line no-console + console.log('Selected tags sent to document:', selectedTagsArray); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error sending selected tags to document:', error); + + // Show error feedback + const originalText = sendSelectedBtn.textContent; + + sendSelectedBtn.textContent = '✗ Error'; + sendSelectedBtn.className = 'btn btn-error'; + sendSelectedBtn.disabled = true; + + setTimeout(() => { + sendSelectedBtn.textContent = originalText; + sendSelectedBtn.className = 'btn btn-primary'; + sendSelectedBtn.disabled = false; + }, 2000); + } + }); + + actionContainer.appendChild(selectAllBtn); + actionContainer.appendChild(deselectAllBtn); + actionContainer.appendChild(sendSelectedBtn); + + // Initial render + renderTagList(taggingData.data.filter((item) => item.value)); + updateSendButton(); + + document.body.appendChild(container); +} + +/** + * Initializes the tags tool + */ +async function init() { + try { + const { context, token, actions } = await DA_SDK; + + // Fetch tagging data + const taggingData = await fetchTaggingData(token, actions, context.org, context.repo); + + // Display tagging data + displayTaggingData(taggingData, actions); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error initializing tags tool:', error); + + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-message'; + errorDiv.textContent = '❌ Error initializing tags tool'; + document.body.appendChild(errorDiv); + } +} + +init();