diff --git a/.DS_Store b/.DS_Store index dedabdf..1f70fdb 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/_playground/table.js b/_playground/table.js index 47eb7af..19acd55 100644 --- a/_playground/table.js +++ b/_playground/table.js @@ -18,6 +18,8 @@ var observableAttributes = [ var OuterbaseEvent = { // The user has triggered an action to save updates onSave: "onSave", + // The user has triggered an action to configure the plugin + configurePlugin: "configurePlugin", } var OuterbaseColumnEvent = { @@ -65,36 +67,24 @@ var OuterbaseTableEvent = { class OuterbasePluginConfig_$PLUGIN_ID { // Inputs from Outerbase for us to retain tableValue = undefined + count = 0 - page = 1 - offset = 50 + limit = 0 + offset = 0 + page = 0 + pageCount = 0 theme = "light" - // Inputs from the configuration screen - imageKey = undefined - optionalImagePrefix = undefined - titleKey = undefined - descriptionKey = undefined - subtitleKey = undefined - // Variables for us to hold state of user actions deletedRows = [] constructor(object) { - this.imageKey = object?.imageKey - this.optionalImagePrefix = object?.optionalImagePrefix - this.titleKey = object?.titleKey - this.descriptionKey = object?.descriptionKey - this.subtitleKey = object?.subtitleKey + } toJSON() { return { - "imageKey": this.imageKey, - "imagePrefix": this.optionalImagePrefix, - "titleKey": this.titleKey, - "descriptionKey": this.descriptionKey, - "subtitleKey": this.subtitleKey + } } } @@ -134,418 +124,436 @@ var decodeAttributeByName = (fromClass, name) => { var templateTable = document.createElement("template") templateTable.innerHTML = ` -
-
- -
-
-` -// Can the above div just be a self closing container:
- -class OuterbasePluginTable_$PLUGIN_ID extends HTMLElement { - static get observedAttributes() { - return observableAttributes + .task-square { + width: 16px; + height: 16px; + border-radius: 4px; + background-color: #66aae3; + } + .avatar-large { + width: 32px; + height: 32px; + border-radius: 9999px; + background-color: #5144a4; + color: white; + line-height: 32px; + text-align: center; + font-size: 14px; + font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + cursor: pointer; + opacity: 0.5; + margin-right: 2px; + } + .avatar-large-selected { + ring: 2; + ring-color: #2152c5; + opacity: 1; } - config = new OuterbasePluginConfig_$PLUGIN_ID({}) + .card-bottom-avatar { + width: 24px; + height: 24px; + border-radius: 9999px; + background-color: #5144a4; + color: white; + line-height: 24px; + text-align: center; + font-size: 10px; + } - constructor() { - super() + .is-dragging { + scale: 1.05; + opacity: 0.5; + } - this.shadow = this.attachShadow({ mode: "open" }) - this.shadow.appendChild(templateTable.content.cloneNode(true)) + .dark { + .kanban-container { + background-color: black; + color: white; + } } - connectedCallback() { - this.render() + .ticket-detail-modal { + display: none; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + background-color: rgba(0, 0, 0, 0.3); } - attributeChangedCallback(name, oldValue, newValue) { - this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) - this.config.tableValue = decodeAttributeByName(this, "tableValue") - this.config.theme = decodeAttributeByName(this, "metadata").theme + .ticket-detail-container { + display: flex; + flex-direction: column; + padding: 24px 32px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 90%; + height: 90%; + background: white; + border-radius: 8px; + box-shadow: rgba(9, 30, 66, 0.03) 0px 8px 12px 0px, rgba(9, 30, 66, 0.1) 0px 0px 1px 0px; + background-clip: border-box; + } - var element = this.shadow.getElementById("theme-container"); - element.classList.remove("dark") - element.classList.add(this.config.theme); + .modal-header-title { + color: #6B778C; + font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + font-family: 14px; + } - this.render() + .modal-header-button { + position: relative; + width: 32px; + height: 32px; + cursor: pointer; } - render() { - this.shadow.querySelector("#container").innerHTML = ` -
-

Welcome to the Outerbase Car Dealership!



View All >

- ${this.config?.tableValue?.length && this.config?.tableValue?.map((row) => ` -
- - ${ this.config.imageKey ? `
` : `` } - -
- ${ this.config.titleKey ? `

${row[this.config.titleKey]}

` : `` } - ${ this.config.subtitleKey ? `

${row[this.config.subtitleKey]}

` : `` } - ${ this.config.descriptionKey ? `

${row[this.config.descriptionKey]}

` : `` } - - -
-
- `).join("")} + .modal-header-button:hover { + background-color: #ebebeb; + border-radius: 4px; + } -
-

What Next?

- - - -
-
- ` + .modal-header-button > svg { + width: 24px; + height: 24px; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } - const deleteRowButtons = this.shadow.querySelectorAll('.deleteRowButton'); - deleteRowButtons.forEach((btn, index) => { - btn.addEventListener('click', () => { - let row = this.config.tableValue[index] - triggerEvent(this, { - action: OuterbaseTableEvent.deleteRow, - value: row - }) - - this.config.deletedRows.push(row) - this.config.tableValue.splice(index, 1) - this.render() - }); - }); + #task-details-title-input { + font-size: 24px; + font-weight: 500; + width: 100%; + margin-top: 8px; + margin-bottom: 16px; + border: 0; + outline: 0; + padding: 8px; + transition: all 0.15s ease-in-out; + border-radius: 4px; + } - const markSoldButtons = this.shadow.querySelectorAll('.markSoldButton'); - markSoldButtons.forEach((btn, index) => { - btn.addEventListener('click', () => { - let row = this.config.tableValue[index] - - fetch( - "https://adjacent-apricot.cmd.outerbase.io/mark-sold", - { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ - id: row.id, - }), - } - ); - - // triggerEvent(this, { - // action: OuterbaseTableEvent.updateRow, - // value: row - // }) - }); - }); + #task-details-title-input:focus { + outline: 2px solid #2152c5; + } - var createRowButton = this.shadow.getElementById("createRowButton"); - createRowButton.addEventListener("click", () => { - let row = { - "id": 0, - "make_id": "Outerbase", - "model": "Spacecar", - "year": 2047, - "vin": "SPCMN404NOREGRETS", - "color": "Purple", - "price": 42069000, - "city": "Pittsburgh", - "state": "Pennsylvania", - "postal": 15203, - "longitude": 58.4767, - "latitude": -16.1003, - "description": "The best space car money can buy.", - "seller": "mr_base", - "seller_name": "Outer Base", - "image": "https://images.unsplash.com/photo-1506469717960-433cebe3f181?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEzMjA3NH0", - "image_thumb": "https://images.unsplash.com/photo-1506469717960-433cebe3f181?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjEzMjA3NH0" - } - this.config.tableValue.push(row) - this.render() - - triggerEvent(this, { - action: OuterbaseTableEvent.createRow, - value: row - }) - }); + #task-details-title-input::placeholder { + color: #6B778C; + } - var previousPageButton = this.shadow.getElementById("previousPageButton"); - previousPageButton.addEventListener("click", () => { - triggerEvent(this, { - action: OuterbaseTableEvent.getPreviousPage, - value: {} - }) - }); + #task-details-title-input::-webkit-input-placeholder { + color: #6B778C; + } - var nextPageButton = this.shadow.getElementById("nextPageButton"); - nextPageButton.addEventListener("click", () => { - triggerEvent(this, { - action: OuterbaseTableEvent.getNextPage, - value: {} - }) - }); + #task-details-title-input::-moz-placeholder { + color: #6B778C; } -} + #task-details-title-input:-ms-input-placeholder { + color: #6B778C; + } -/** - * ****************** - * Configuration View - * ****************** - * - * ░░░░░░░░░░░░░░░░░ - * ░░░░░▀▄░░░▄▀░░░░░ - * ░░░░▄█▀███▀█▄░░░░ - * ░░░█▀███████▀█░░░ - * ░░░█░█▀▀▀▀▀█░█░░░ - * ░░░░░░▀▀░▀▀░░░░░░ - * ░░░░░░░░░░░░░░░░░ - * - * When a user either installs a plugin onto a table resource for the first time - * or they configure an existing installation, this is the view that is presented - * to the user. For many plugin applications it's essential to capture information - * that is required to allow your plugin to work correctly and this is the best - * place to do it. - * - * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` - * event exists so Outerbase can complete the installation or preference update - * action. - */ -var templateConfiguration = document.createElement("template") -templateConfiguration.innerHTML = ` -
-
- +
+ +
+ +
+
+ + +
` -// Can the above div just be a self closing container:
-class OuterbasePluginTableConfiguration_$PLUGIN_ID extends HTMLElement { +class OuterbasePluginTable_$PLUGIN_ID extends HTMLElement { static get observedAttributes() { return observableAttributes } config = new OuterbasePluginConfig_$PLUGIN_ID({}) + people = [] + selectedPeople = [] + + showBugs = true + showFeatures = true + showSupport = true + + backlog = [] + progress = [] + blocked = [] + qa = [] + done = [] + + showDetailsModalRecentErrors = false + constructor() { super() this.shadow = this.attachShadow({ mode: "open" }) - this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + this.shadow.appendChild(templateTable.content.cloneNode(true)) + + this.getAllPeople() + this.getAllTasks() } connectedCallback() { @@ -553,9 +561,7 @@ class OuterbasePluginTableConfiguration_$PLUGIN_ID extends HTMLElement { } attributeChangedCallback(name, oldValue, newValue) { - this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) this.config.tableValue = decodeAttributeByName(this, "tableValue") - this.config.theme = decodeAttributeByName(this, "metadata").theme var element = this.shadow.getElementById("theme-container"); element.classList.remove("dark") @@ -564,88 +570,633 @@ class OuterbasePluginTableConfiguration_$PLUGIN_ID extends HTMLElement { this.render() } + async getAllPeople() { + let response = await fetch("https://absolute-teal.cmd.outerbase.io/people", { + method: "GET" + }) + + let json = await response.json() + this.people = json.response.items + this.selectedPeople = Object.assign([], json.response.items) + + this.render() + } + + async getAllTasks() { + let response = await fetch("https://absolute-teal.cmd.outerbase.io/tasks", { + method: "GET" + }) + + let json = await response.json() + this.config.tableValue = json.response.items + + this.render() + } + + async updateTask(id, status) { + fetch("https://absolute-teal.cmd.outerbase.io/task/status", { + method: "PUT", + body: JSON.stringify({ + id: id, + status: status, + }) + }) + } + + setupCheck() { + this.shadow.appendChild(templateTable.content.cloneNode(true)) + } + render() { - let sample = this.config.tableValue.length ? this.config.tableValue[0] : {} - let keys = Object.keys(sample) - - if (!keys || keys.length === 0 || !this.shadow.querySelector('#configuration-container')) return - - this.shadow.querySelector('#configuration-container').innerHTML = ` -
-

Image Key

- - -

Image URL Prefix (optional)

- - -

Title Key

- - -

Description Key

- - -

Subtitle Key

- - -
- + this.setupCheck() + + this.backlog = this.config.tableValue?.filter((item) => { return item.status?.toLowerCase() === "backlog" }) + this.progress = this.config.tableValue?.filter((item) => { return item.status?.toLowerCase() === "in progress" }) + this.blocked = this.config.tableValue?.filter((item) => { return item.status?.toLowerCase() === "blocked" }) + this.qa = this.config.tableValue?.filter((item) => { return item.status?.toLowerCase() === "qa" }) + this.done = this.config.tableValue?.filter((item) => { return item.status?.toLowerCase() === "done" }) + + const selectedPeopleIds = new Set(this.selectedPeople.map(person => person.id)); + const supportedTypes = new Set([this.showBugs ? "bug" : undefined, this.showFeatures ? "task" : undefined, this.showSupport ? "support" : undefined]); + + this.shadow.querySelector(".kanban-container").innerHTML = ` +
+ + ` + this.people?.map((person, index) => + `
+ ${person.first_name[0] + person.last_name[0]} +
` + ).join("") + ` + +
+ + + + + + +
+ + +
+ +
-
+ +
+
+
Backlog (${this.backlog.filter((item) => { + return item.status?.toLowerCase() === "backlog" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).length})
+ +
+ ` + this.config.tableValue?.filter((item) => { + return item.status?.toLowerCase() === "backlog" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).map((row) => { + return this.createCardElement({ + title: row.title, + priority: row.priority, + type: row.type, + id: row.id, + assignee: row.assignee, + status: row.status + }) + }).join("") + ` +
+
+ +
+
In Progress (${this.progress.filter((item) => { + return item.status?.toLowerCase() === "in progress" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).length})
+ +
+ ` + this.config.tableValue?.filter((item) => { + return item.status?.toLowerCase() === "in progress" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).map((row) => { + return this.createCardElement({ + title: row.title, + priority: row.priority, + type: row.type, + id: row.id, + assignee: row.assignee, + status: row.status + }) + }).join("") + ` +
+
-
-
- +
+
Blocked (${this.blocked.filter((item) => { + return item.status?.toLowerCase() === "blocked" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).length})
+ +
+ ` + this.config.tableValue?.filter((item) => { + return item.status?.toLowerCase() === "blocked" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).map((row) => { + return this.createCardElement({ + title: row.title, + priority: row.priority, + type: row.type, + id: row.id, + assignee: row.assignee, + status: row.status + }) + }).join("") + ` +
+
+ +
+
QA (${this.qa.filter((item) => { + return item.status?.toLowerCase() === "qa" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).length})
+ +
+ ` + this.config.tableValue?.filter((item) => { + return item.status?.toLowerCase() === "qa" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).map((row) => { + return this.createCardElement({ + title: row.title, + priority: row.priority, + type: row.type, + id: row.id, + assignee: row.assignee, + status: row.status + }) + }).join("") + ` +
+
-
-

${sample[this.config.titleKey]}

-

${sample[this.config.descriptionKey]}

-

${sample[this.config.subtitleKey]}

+
+
Done (${this.done.filter((item) => { + return item.status?.toLowerCase() === "done" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).length})
+ +
+ ` + this.config.tableValue?.filter((item) => { + return item.status?.toLowerCase() === "done" && + supportedTypes.has(item.type?.toLowerCase()) && + this.selectedPeople?.find((person) => person.first_name + " " + person.last_name === item.assignee) + }).map((row) => { + return this.createCardElement({ + title: row.title, + priority: row.priority, + type: row.type, + id: row.id, + assignee: row.assignee, + status: row.status + }) + }).join("") + ` +
-
` - var saveButton = this.shadow.getElementById("saveButton"); - saveButton.addEventListener("click", () => { - triggerEvent(this, { - action: OuterbaseEvent.onSave, - value: this.config.toJSON() + // When a card item is clicked then show the modal with details at `createDetailsModal(task)` + this.shadow.querySelectorAll(".card").forEach((element) => { + element.addEventListener("click", () => { + // Get data-ticket-id + let taskId = element.getAttribute("data-ticket-id") + + // Find task in `tableValue` array + let task = this.config.tableValue?.find((row) => row.id + "" === taskId) + + this.createDetailsModal(task) }) - }); + }) - var imageKeySelect = this.shadow.getElementById("imageKeySelect"); - imageKeySelect.addEventListener("change", () => { - this.config.imageKey = imageKeySelect.value + // Find the class with ID of `create-ticket-button` and add an onClick event listener to it + this.shadow.querySelector(".create-ticket-button").addEventListener("click", () => { + // Unhide the modal + this.createDetailsModal() + }) + + // Find the button with ID of `show-bugs-button` and add an onClick event listener to it + this.shadow.getElementById("show-bugs-button").addEventListener("click", () => { + this.showBugs = !this.showBugs this.render() - }); + }) - var titleKeySelect = this.shadow.getElementById("titleKeySelect"); - titleKeySelect.addEventListener("change", () => { - this.config.titleKey = titleKeySelect.value + // Find the button with ID of `show-features-button` and add an onClick event listener to it + this.shadow.getElementById("show-features-button").addEventListener("click", () => { + this.showFeatures = !this.showFeatures this.render() - }); + }) - var descriptionKeySelect = this.shadow.getElementById("descriptionKeySelect"); - descriptionKeySelect.addEventListener("change", () => { - this.config.descriptionKey = descriptionKeySelect.value + // Find the button with ID of `show-supports-button` and add an onClick event listener to it + this.shadow.getElementById("show-supports-button").addEventListener("click", () => { + this.showSupport = !this.showSupport this.render() + }) + + // Find all DOM elements that have `data-person-id` attribute and add an onClick event listener to them + this.shadow.querySelectorAll("[data-person-id]").forEach((element) => { + element.addEventListener("click", () => { + let personId = element.getAttribute("data-person-id") + let person = this.people?.find((person) => person.id + "" === personId) + + // If person exists in `selectedPeople` array, remove them + let selectedPersonIndex = this.selectedPeople.findIndex((person) => person.id + "" === personId) + + if (selectedPersonIndex > -1) { + this.selectedPeople.splice(selectedPersonIndex, 1) + } else { + this.selectedPeople.push(person) + } + + this.render() + }) }); - var subtitleKeySelect = this.shadow.getElementById("subtitleKeySelect"); - subtitleKeySelect.addEventListener("change", () => { - this.config.subtitleKey = subtitleKeySelect.value - this.render() + var draggables = this.shadow.querySelectorAll(".card"); + var droppables = this.shadow.querySelectorAll(".grid-item"); + + draggables.forEach((task) => { + task.addEventListener("dragstart", () => { + task.classList.add("is-dragging"); + }); + + task.addEventListener("dragend", () => { + task.classList.remove("is-dragging"); + + console.log('taskId: ', task.getAttribute("data-ticket-id")) + console.log('columnId: ', task.parentElement.getAttribute("data-column")) + + let taskId = task.getAttribute("data-ticket-id") + let columnId = task.parentElement.getAttribute("data-column") + let currentTask = this.config.tableValue?.find((row) => row.id + "" === taskId) + var status; + + // See if task exists in `backlog` array and if so, remove it + let index = this.backlog.findIndex((row) => row.id + "" === taskId) + if (index > -1) { + this.backlog.splice(index, 1) + } + + // See if task exists in `progress` array and if so, remove it + index = this.progress.findIndex((row) => row.id + "" === taskId) + if (index > -1) { + this.progress.splice(index, 1) + } + + // See if task exists in `blocked` array and if so, remove it + index = this.blocked.findIndex((row) => row.id + "" === taskId) + if (index > -1) { + this.blocked.splice(index, 1) + } + + // See if task exists in `qa` array and if so, remove it + index = this.qa.findIndex((row) => row.id + "" === taskId) + if (index > -1) { + this.qa.splice(index, 1) + } + + // See if task exists in `done` array and if so, remove it + index = this.done.findIndex((row) => row.id + "" === taskId) + if (index > -1) { + this.done.splice(index, 1) + } + + switch (columnId) { + case "backlog": + status = "Backlog" + this.backlog.push(currentTask) + break; + case "in-progress": + status = "In Progress" + this.progress.push(currentTask) + break; + case "blocked": + status = "Blocked" + this.blocked.push(currentTask) + break; + case "qa": + status = "QA" + this.qa.push(currentTask) + break; + case "done": + status = "Done" + this.done.push(currentTask) + break; + default: + status = "Backlog" + this.backlog.push(currentTask) + break; + } + + // Find titles of columns and update the count + let backlogCount = this.shadow.querySelector(".column-title-backlog") + let progressCount = this.shadow.querySelector(".column-title-progress") + let blockedCount = this.shadow.querySelector(".column-title-blocked") + let qaCount = this.shadow.querySelector(".column-title-qa") + let doneCount = this.shadow.querySelector(".column-title-done") + + backlogCount.innerHTML = `Backlog (${this.backlog.length})` + progressCount.innerHTML = `In Progress (${this.progress.length})` + blockedCount.innerHTML = `Blocked (${this.blocked.length})` + qaCount.innerHTML = `QA (${this.qa.length})` + doneCount.innerHTML = `Done (${this.done.length})` + + // console.log('Table: ', this.config.tableValue) + // let row = this.config.tableValue?.find((row) => row.id + "" === taskId) + // console.log('Row: ', row) + + // if (row) { + // row.status = status + + // triggerEvent(this, { + // action: OuterbaseTableEvent.updateRow, + // value: row + // }) + // } + + // Call API to update task + this.updateTask(currentTask.id, status) + }); }); + + droppables.forEach((zone) => { + zone.addEventListener("dragover", (e) => { + e.preventDefault(); + + const bottomTask = insertAboveTask(zone, e.clientY); + const curTask = this.shadow.querySelector(".is-dragging"); + + if (!bottomTask) { + zone.appendChild(curTask); + } else { + zone.insertBefore(curTask, bottomTask); + } + }); + }); + + const insertAboveTask = (zone, mouseY) => { + const els = zone.querySelectorAll(".card:not(.is-dragging)"); + + let closestTask = null; + let closestOffset = Number.NEGATIVE_INFINITY; + + els.forEach((task) => { + const { top } = task.getBoundingClientRect(); + const offset = mouseY - top; + + if (offset < 0 && offset > closestOffset) { + closestOffset = offset; + closestTask = task; + } + }); + + return closestTask; + }; + } + + createCardElement({ title, priority, type, id, assignee, status }) { + let person = this.people?.find((person) => person.first_name + " " + person.last_name === assignee) + let initials = assignee.split(" ").map((name) => name[0]).join("") + initials = initials.length > 2 ? initials.slice(0, 2) : initials + + return (` +
+
+ ${title} +
+ +
+
+
+
+
+
+
+ +
+
+
OUT-${id}
+
${initials}
+
+
+ `) + } + + createDetailsModal(task) { + console.log('Task: ', task) + + // Find the element identified by `ticket-detail-modal` and change it's display to `block` + this.shadow.querySelector(".ticket-detail-modal").style.display = "block" + + var priority = 2; + + this.shadow.querySelector(".ticket-detail-container").innerHTML = ` + +
+
+ + +
+ + + + +
+ +
+ + + +
+ +
+
+ +
+ ` + + // Find `recent-errors-title` by ID and add an onClick event listener to it + this.shadow.getElementById("recent-errors-title").addEventListener("click", () => { + this.showDetailsModalRecentErrors = !this.showDetailsModalRecentErrors + this.createDetailsModal(task) + }) + + // Find button with id `close-modal` and add an onClick event listener to it + this.shadow.getElementById("close-modal").addEventListener("click", () => { + // Find the element identified by `ticket-detail-modal` and change it's display to `none` + this.shadow.querySelector(".ticket-detail-modal").style.display = "none" + }) } } -window.customElements.define("outerbase-plugin-table", OuterbasePluginTable_$PLUGIN_ID) -window.customElements.define("outerbase-plugin-configuration", OuterbasePluginTableConfiguration_$PLUGIN_ID) \ No newline at end of file +window.customElements.define('outerbase-plugin-table', OuterbasePluginTable_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-table-$PLUGIN_ID', OuterbasePluginTable_$PLUGIN_ID) diff --git a/columns/_empty.js b/columns/_empty.js index ea7fdca..9d4dbbe 100644 --- a/columns/_empty.js +++ b/columns/_empty.js @@ -1,4 +1,4 @@ -var observableAttributes = [ +var observableAttributes_$PLUGIN_ID = [ // The value of the cell that the plugin is being rendered in "cellValue", // The configuration object that the user specified when installing the plugin @@ -7,12 +7,12 @@ var observableAttributes = [ "metadata" ] -var OuterbaseEvent = { +var OuterbaseEvent_$PLUGIN_ID = { // The user has triggered an action to save updates onSave: "onSave", } -var OuterbaseColumnEvent = { +var OuterbaseColumnEvent_$PLUGIN_ID = { // The user has began editing the selected cell onEdit: "onEdit", // Stops editing a cells editor popup view and accept the changes @@ -49,7 +49,7 @@ class OuterbasePluginConfig_$PLUGIN_ID { } } -var triggerEvent = (fromClass, data) => { +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { const event = new CustomEvent("custom-change", { detail: data, bubbles: true, @@ -59,7 +59,7 @@ var triggerEvent = (fromClass, data) => { fromClass.dispatchEvent(event); } -var decodeAttributeByName = (fromClass, name) => { +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { const encodedJSON = fromClass.getAttribute(name); const decodedJSON = encodedJSON ?.replace(/"/g, '"') @@ -116,7 +116,7 @@ templateCell_$PLUGIN_ID.innerHTML = ` class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { static get observedAttributes() { - return privileges + return observableAttributes_$PLUGIN_ID } config = new OuterbasePluginConfig_$PLUGIN_ID({}) @@ -129,7 +129,7 @@ class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { } connectedCallback() { - this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) this.render() } @@ -170,7 +170,7 @@ templateEditor_$PLUGIN_ID.innerHTML = ` class OuterbasePluginCellEditor_$PLUGIN_ID extends HTMLElement { static get observedAttributes() { - return privileges + return observableAttributes_$PLUGIN_ID } config = new OuterbasePluginConfig_$PLUGIN_ID({}) @@ -183,8 +183,8 @@ class OuterbasePluginCellEditor_$PLUGIN_ID extends HTMLElement { } connectedCallback() { - this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) - this.config.cellValue = decodeAttributeByName(this, "cellValue") + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") this.render() } @@ -234,7 +234,7 @@ templateConfiguration.innerHTML = ` class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { static get observedAttributes() { - return privileges + return observableAttributes_$PLUGIN_ID } config = new OuterbasePluginConfig_$PLUGIN_ID({}) @@ -247,8 +247,8 @@ class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { } connectedCallback() { - this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) - this.config.cellValue = decodeAttributeByName(this, "cellValue") + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") this.render() } @@ -262,7 +262,7 @@ class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { var saveButton = this.shadow.getElementById("saveButton"); saveButton.addEventListener("click", () => { - triggerEvent(this, { + triggerEvent_$PLUGIN_ID(this, { action: OuterbaseEvent.onSave, value: {} }) diff --git a/columns/column-config.js b/columns/column-config.js new file mode 100644 index 0000000..60052c9 --- /dev/null +++ b/columns/column-config.js @@ -0,0 +1,403 @@ +var observableAttributes = [ + // The value of the cell that the plugin is being rendered in + "cellvalue", + // The value of the row that the plugin is being rendered in + "rowvalue", + // The value of the table that the plugin is being rendered in + "tablevalue", + // The schema of the table that the plugin is being rendered in + "tableschemavalue", + // The schema of the database that the plugin is being rendered in + "databaseschemavalue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var privileges = [ + 'cellValue', + 'configuration', +] + +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ + +
+` + +var templateEditor_$PLUGIN_ID = document.createElement('template') +templateEditor_$PLUGIN_ID.innerHTML = ` + + +
+
+ +
+
+` + +// This is the configuration object that Outerbase passes to your plugin. +// Define all of the configuration options that your plugin requires here. +class OuterbasePluginConfig_$PLUGIN_ID { + constructor(object) { + // No custom properties needed in this plugin. + } +} + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + // The shadow DOM is a separate DOM tree that is attached to the element. + // This allows us to encapsulate our styles and markup. It also prevents + // styles from the parent page from leaking into our plugin. + this.shadow = this.attachShadow({ mode: 'open' }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + // This function is called when the UI is made available into the DOM. Put any + // logic that you want to run when the element is first stood up here, such as + // event listeners, default values to display, etc. + connectedCallback() { + // Parse the configuration object from the `configuration` attribute + // and store it in the `config` property. + this.config = new OuterbasePluginConfig_$PLUGIN_ID( + JSON.parse(this.getAttribute('configuration')) + ) + + // Set default value based on input + this.shadow.querySelector('#image-value').value = this.getAttribute('cellvalue') + + var imageInput = this.shadow.getElementById("image-value"); + var viewImageButton = this.shadow.getElementById("view-image"); + + if (imageInput && viewImageButton) { + imageInput.addEventListener("focus", () => { + // Tell Outerbase to start editing the cell + this.callCustomEvent({ + action: 'onstopedit', + value: true + }) + }); + + imageInput.addEventListener("blur", () => { + // Tell Outerbase to update the cells raw value + this.callCustomEvent({ + action: 'cellvalue', + value: imageInput.value + }) + + // Then stop editing the cell and close the editor view + this.callCustomEvent({ + action: 'onstopedit', + value: true + }) + }); + + viewImageButton.addEventListener("click", () => { + this.callCustomEvent({ + action: 'onedit', + value: true + }) + }); + } + } + + callCustomEvent(data) { + const event = new CustomEvent('custom-change', { + detail: data, + bubbles: true, // If you want the event to bubble up through the DOM + composed: true // Allows the event to pass through shadow DOM boundaries + }); + + this.dispatchEvent(event); + } +} + +class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + constructor() { + super() + + // The shadow DOM is a separate DOM tree that is attached to the element. + // This allows us to encapsulate our styles and markup. It also prevents + // styles from the parent page from leaking into our plugin. + this.shadow = this.attachShadow({ mode: 'open' }) + this.shadow.appendChild(templateEditor_$PLUGIN_ID.content.cloneNode(true)) + + // Parse the configuration object from the `configuration` attribute + // and store it in the `config` property. + this.config = new OuterbasePluginConfig_$PLUGIN_ID( + JSON.parse(this.getAttribute('configuration')) + ) + } + + // This function is called when the UI is made available into the DOM. Put any + // logic that you want to run when the element is first stood up here, such as + // event listeners, default values to display, etc. + connectedCallback() { + var imageView = this.shadow.getElementById("image"); + var backgroundImageView = this.shadow.getElementById("background-image"); + + if (imageView && backgroundImageView) { + imageView.src = this.getAttribute('cellvalue') + backgroundImageView.style.backgroundImage = `url(${this.getAttribute('cellvalue')})` + } + } +} + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration = document.createElement("template") +templateConfiguration.innerHTML = ` + + +
+
+ +
+
+` +// Can the above div just be a self closing container:
+ +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + } + + connectedCallback() { + this.render() + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) + this.config.tableValue = decodeAttributeByName(this, "tableValue") + this.config.theme = decodeAttributeByName(this, "metadata").theme + + var element = this.shadow.getElementById("theme-container"); + element.classList.remove("dark") + element.classList.add(this.config.theme); + + this.render() + } + + render() { + let sample = this.config.tableValue.length ? this.config.tableValue[0] : {} + let keys = Object.keys(sample) + + if (!keys || keys.length === 0 || !this.shadow.querySelector('#configuration-container')) return + + this.shadow.querySelector('#configuration-container').innerHTML = ` +
+

Image URL Prefix (optional)

+ + +
+ +
+
+ ` + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + triggerEvent(this, { + action: OuterbaseEvent.onSave, + value: { "test": "me aht" } + }) + }); + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-editor-$PLUGIN_ID', OuterbasePluginEditor_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) diff --git a/columns/hackathon/boolean.js b/columns/hackathon/boolean.js new file mode 100644 index 0000000..68500f5 --- /dev/null +++ b/columns/hackathon/boolean.js @@ -0,0 +1,255 @@ +var privileges_$PLUGIN_ID = [ + 'cellValue', + 'configuration', +] + +var observableAttributes = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var OuterbaseEvent = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + theme = "light" + + constructor(object) { + this.theme = object?.theme ? object.theme : "light"; + } +} + +var triggerEvent = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ + + +
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + + // If user clicks on `.indicators` then update the cell value to go from "true" > "false" > "null" depending on current value + this.shadow.querySelector("svg").addEventListener("click", () => { + this.render(); + }); + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + + render() { + // let cellValue = this.getAttribute('cellvalue') + + // if (cellValue.length === 0) { + // cellValue = "NULL" + // } + + // this.shadow.querySelector('span').innerText = cellValue + + console.log('Render') + // let currentValue = this.getAttribute("cellvalue").toLowerCase(); + let currentValue = this.shadow.querySelector('span').innerText.toUpperCase(); + let newValue = "TRUE" + + // this.shadow.querySelector("#indicator-true").classList.remove("indicator-selected") + // this.shadow.querySelector("#indicator-false").classList.remove("indicator-selected") + // this.shadow.querySelector("#indicator-empty").classList.remove("indicator-selected") + + if (currentValue === "TRUE") { + newValue = "FALSE" + // this.shadow.querySelector("#indicator-false").classList.add("indicator-selected") + } else if (currentValue === "FALSE") { + newValue = "NULL" + // this.shadow.querySelector("#indicator-empty").classList.add("indicator-selected") + } else { + newValue = "TRUE" + // this.shadow.querySelector("#indicator-true").classList.add("indicator-selected") + } + + // Set value of span + this.shadow.querySelector('span').innerText = newValue + + // triggerEvent(this, { + // type: OuterbaseColumnEvent.updateCell, + // data: newValue + // }) + } +} + + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) \ No newline at end of file diff --git a/columns/hackathon/date.js b/columns/hackathon/date.js new file mode 100644 index 0000000..f15e52b --- /dev/null +++ b/columns/hackathon/date.js @@ -0,0 +1,300 @@ +var observableAttributes_$PLUGIN_ID = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + theme = "light" + + constructor(object) { + this.theme = object?.theme ? object.theme : "light"; + } +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ Jan 3 202410:03 PM + +
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + + // When the SVG is clicked, we want to trigger an event to the parent + this.shadow.querySelector('span').addEventListener('click', () => { + triggerEvent_$PLUGIN_ID(this, { + action: "onedit", + value: true, + }) + }) + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + + render() { + // Get the cellValue + const cellValue = this.getAttribute("cellValue") + + // Cast the cellValue into a date + const date = new Date(cellValue) + + // The `date` is in UTC, so we need to convert it to the local timezone + date.setMinutes(date.getMinutes() - date.getTimezoneOffset()) + + + // Format a date string with `MMM d yyyy` + let dateString = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric" + }) + + // Remove comma from dateString + dateString = dateString.replace(",", "") + + // Format another date string with `h:mm a` + const timeString = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "numeric", + hour12: true + }) + + // Set `#date` to dateString + this.shadow.querySelector("#date").textContent = dateString + ',' + + // Set `#time` to timeString + this.shadow.querySelector("#time").textContent = timeString + } +} + + +// For Configuration view let them choose the SOURCE date (e.g. UTC) and the +// TARGET date (e.g. Local Timezone) and the format of the date and time. + + + + +var templateEditor_$PLUGIN_ID = document.createElement("template"); +templateEditor_$PLUGIN_ID.innerHTML = ` + + +
+ +
+`; + +class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID; + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateEditor_$PLUGIN_ID.content.cloneNode(true)) + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + + this.render() + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + } + + render() { + + } +} + + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-editor-$PLUGIN_ID', OuterbasePluginEditor_$PLUGIN_ID) \ No newline at end of file diff --git a/columns/hackathon/enum.js b/columns/hackathon/enum.js new file mode 100644 index 0000000..3afbd04 --- /dev/null +++ b/columns/hackathon/enum.js @@ -0,0 +1,643 @@ +var privileges_$PLUGIN_ID = [ + 'cellValue', + 'configuration', +] + +var observableAttributes_$PLUGIN_ID = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var OuterbaseEvent_$PLUGIN_ID = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent_$PLUGIN_ID = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + theme = "light" + enumOptions = [] + + constructor(object) { + this.theme = object?.theme ? object.theme : "light"; + this.enumOptions = object?.enumOptions ? object.enumOptions : []; + } +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+
+
+ +
+
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + + this.shadow.querySelector('#inner').addEventListener('click', () => { + triggerEvent_$PLUGIN_ID(this, { + action: "onedit", + value: true, + }) + }) + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + + render() { + let cellValue = this.getAttribute('cellvalue') + + if (cellValue.length === 0) { + cellValue = "NULL" + } + + this.shadow.querySelector('#label').innerText = cellValue + } +} + + + + + + + +/** + * **************** + * Cell Editor View + * **************** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * An optional view that pops below the cell for an expanded viewing area + * of additional UI data. + */ +var templateEditor_$PLUGIN_ID = document.createElement("template") +templateEditor_$PLUGIN_ID.innerHTML = ` + + +
+ +
+ +
+
+` + +class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateEditor_$PLUGIN_ID.content.cloneNode(true)) + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + + this.render() + } + + connectedCallback() { + // this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + // this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") + this.render() + } + + render() { + // const options = ['Option 1', 'Option 2', 'Option 3'] + const options = this.config.enumOptions + const optionsContainer = this.shadow.querySelector('#options') + + // Remove all existing options + optionsContainer.innerHTML = '' + + options.forEach(option => { + const optionElement = this.createOptionElement(option) + optionsContainer.appendChild(optionElement) + }) + + // When a user clicks an `option` element, trigger an event + optionsContainer.querySelectorAll('.option').forEach((option, index) => { + option.addEventListener('click', () => { + triggerEvent_$PLUGIN_ID(this, { + action: "updatecell", + value: options[index], + }) + }) + }) + } + + createOptionElement(name) { + // Create a DIV element + let div = document.createElement('div') + div.innerHTML = ` +
+
+ ${name} +
+
+ ` + + return div + } +} + + + + + + + + + + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration = document.createElement("template") +templateConfiguration.innerHTML = ` + + +
+

Select Enum Options

+ +
+ +
+ +
+
+ +
+ +
+ + Add Option +
+ +
+ + Save View +
+` + +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + newOptions = [] + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + // Combine the existing `enumOptions` with the `newOptions` array + this.config.enumOptions = this.config.enumOptions.concat(this.newOptions) + + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseEvent_$PLUGIN_ID.onSave, + value: this.config + }) + }); + + // Listen to when the `add-new-option` button is clicked + this.shadow.querySelector('#add-new-option').addEventListener('click', () => { + let value = this.shadow.querySelector('#current-new-option').value + this.newOptions.push(value) + this.shadow.querySelector('#current-new-option').value = "" + + this.render(); + }) + + this.fetchDistinctValues() + this.render() + } + + // attributeChangedCallback(name, oldValue, newValue) { + // this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + // let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + // this.config.theme = metadata?.theme + + // var element = this.shadow.querySelector(".theme-container") + // element.classList.remove("dark") + // element.classList.add(this.config.theme); + + // this.render() + // } + + async fetchDistinctValues() { + try { + // Based on the information provided to our plugin, we need to identify + // what the column constraints are and what database table it is linked to. + // This will allow us to construct a SQL query to fetch the value from the + // linked table. + const column = this.getAttribute('columnName') + const table = JSON.parse(this.getAttribute('tableSchemaValue')).name + const schema = JSON.parse(this.getAttribute('tableSchemaValue')).schema ?? "public" + + // Necessary information is graciously stored by Outerbase in the `localStorage` + // for us to make the necessary network request to fetch the value from the linked table. + const session = JSON.parse(localStorage.getItem('session')) + const workspaceId = localStorage.getItem('workspace_id') + const sourceId = localStorage.getItem('source_id') + + // SELECT DISTINCT column_name FROM table_name; + + // When a cached value does not exist for this `schema.table.column.value`, fetch the value + // from the database and store it in the cache for future re-use. + await fetch(`https://app.dev.outerbase.com/api/v1/workspace/${workspaceId}/source/${sourceId}/query/raw`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': session?.state?.session?.token + }, + body: JSON.stringify({ + query: `SELECT DISTINCT ${column} FROM ${schema}.${table};`, + options: {} + }) + }).then(response => response.json()).then(data => { + const items = data.response?.items ?? [] + + // Condense the above `items` array with the structure above into an array of strings + let itemsArray = items.map(item => item[column]) + this.config.enumOptions = itemsArray + + this.render(); + }) + } catch (error) { + console.error(error) + } + } + + render() { + const items = this.config.enumOptions + let select = this.shadow.querySelector('#options') + select.innerHTML = '' + + items.forEach(item => { + const div = document.createElement('div') + div.innerHTML = ` +
+
+ ${item} +
+ + +
+ ` + select.appendChild(div) + }) + + this.newOptions.forEach(item => { + const div = document.createElement('div') + div.innerHTML = ` +
+
+ ${item} +
+ + +
+ ` + select.appendChild(div) + }) + + // Listen to the Remove buttons and remove from list when clicked + select.querySelectorAll('.remove-option').forEach((removeButton, index) => { + removeButton.addEventListener('click', () => { + if (index < items.length) { + this.config.enumOptions.splice(index, 1) + } else { + this.newOptions.splice(index - items.length, 1) + } + + this.render() + }) + }) + } +} + + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-editor', OuterbasePluginEditor_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-configuration', OuterbasePluginConfiguration_$PLUGIN_ID) + +// window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-editor-$PLUGIN_ID', OuterbasePluginEditor_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) diff --git a/columns/hackathon/expandable-text.js b/columns/hackathon/expandable-text.js new file mode 100644 index 0000000..b15348d --- /dev/null +++ b/columns/hackathon/expandable-text.js @@ -0,0 +1,510 @@ +var observableAttributes_$PLUGIN_ID = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var OuterbaseEvent = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + cellValue = undefined + + constructor(object) { + + } +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("plugin-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'") + ?.replace(/`/g, "`"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ + +// #container { +// height: 100%; +// min-height: 34px; +// width: calc(100% - 16px); +// padding: 0 8px; +// position: relative; +// display: flex; +// align-items: center; +// gap: 0px; +// } + +// #container { +// height: 100%; +// min-height: 34px; +// position: absolute; +// top: 0; +// left: 12px; +// right: 8px; +// bottom: 0; +// display: flex; +// align-items: center; +// gap: 0px; +// transform: translateY(-1px); +// } +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ + +
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + + // When the SVG is clicked, we want to trigger an event to the parent + this.shadow.querySelector('svg').addEventListener('click', () => { + triggerEvent_$PLUGIN_ID(this, { + action: "onedit", + value: true, + }) + }) + + // Listen to paste event on input + this.shadow.querySelector('input').addEventListener('paste', (event) => { + event.preventDefault() + let text = event.clipboardData.getData('text/plain') + document.execCommand('insertText', false, text) + + // Escape single and double quotes from `text` + // text = JSON.stringify(text) + // ?.replace(/"/g, '"') + // .replace(/'/g, ''') + + // // Remove quotes around the text + // text = text.substring(1, text.length - 1) + + // Send the event to the parent + triggerEvent_$PLUGIN_ID(this, { + action: "updatecell", + value: text, + }) + }) + + // Detect when input value changes + this.shadow.querySelector('input').addEventListener('input', (event) => { + let cellValue = event.target.value + + // Escape quotes from cellValue + // cellValue = JSON.stringify(cellValue) + // ?.replace(/"/g, '"') + // .replace(/'/g, ''') //cellValue.replace(/"/g, '\\"')//.replace(/'/g, "\\'").replace(/`/g, "\\`").replace(/\\/g, "\\\\") + + // Set the input value to the cell value + this.setAttribute('cellvalue', cellValue) + this.shadow.querySelector('input').value = cellValue + + // Send the event to the parent + triggerEvent_$PLUGIN_ID(this, { + action: "updatecell", + value: cellValue, + }) + }) + } + + render() { + let cellValue = this.getAttribute('cellvalue') + + if (cellValue.length === 0 || (cellValue && cellValue.toLowerCase() === "null")) { + this.shadow.querySelector('input').placeholder = "NULL" + } else { + this.shadow.querySelector('input').value = cellValue + } + } +} + + + + + + + + + + +var templateEditor_$PLUGIN_ID = document.createElement("template"); +templateEditor_$PLUGIN_ID.innerHTML = ` + + +
+ + + +
+`; + +class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID; + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + tableSchema = {} + metadata = {} + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateEditor_$PLUGIN_ID.content.cloneNode(true)) + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + + this.render() + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.tableSchema = decodeAttributeByName_$PLUGIN_ID(this, "tableschemavalue") + this.metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + const columnName = this.getAttribute('columnname') + this.render() + + const availableColumns = this.tableSchema.columns + + // Get the column object from the table schema + const column = availableColumns?.find(column => column.name === columnName) + this.maximumCharacterCount = column?.character_maximum_length || null + this.updateCharacterCount() + + // Listen to input changes in textarea + this.shadow.querySelector('textarea').addEventListener('input', (event) => { + const cellValue = event.target.value + + if (cellValue.length === 0 || (cellValue && cellValue.toLowerCase() === "null")) { + this.shadow.querySelector('#null-placeholder').style.display = "block" + } else { + this.shadow.querySelector('#null-placeholder').style.display = "none" + } + + this.updateCharacterCount() + }) + + // Listen to `update-button` and `cancel-button` clicks + this.shadow.querySelector('#update-button').addEventListener('click', () => { + // Get value of textarea + let value = this.shadow.querySelector('textarea').value + + triggerEvent_$PLUGIN_ID(this, { + action: "updatecell", + value, + }) + + triggerEvent_$PLUGIN_ID(this, { + action: "onstopedit" + }) + + // Close the editor after event has saved changes + setTimeout(() => { + triggerEvent_$PLUGIN_ID(this, { + action: "onstopedit" + }) + }, 500); + }) + + this.shadow.querySelector('#cancel-button').addEventListener('click', () => { + triggerEvent_$PLUGIN_ID(this, { + action: "oncanceledit", + value: true, + }) + }) + } + + render() { + // Get the `cellValue` and populate it in the `textarea` + let cellValue = this.getAttribute('cellvalue') + this.shadow.querySelector('textarea').value = cellValue + + if (cellValue.length === 0 || (cellValue && cellValue.toLowerCase() === "null")) { + // this.shadow.querySelector('textarea').placeholder = "NULL" + this.shadow.querySelector('textarea').value = "" + this.shadow.querySelector('#null-placeholder').style.display = "block" + } else { + this.shadow.querySelector('textarea').value = cellValue + this.shadow.querySelector('#null-placeholder').style.display = "none" + } + + // If `this.metadata.editable` is false, hide the button + if (this.metadata.editable === false) { + this.shadow.querySelector('#footer').style.display = "none" + + // Set textarea to readonly + this.shadow.querySelector('textarea').readOnly = true + } else { + this.shadow.querySelector('#footer').style.display = "flex" + + // Set textarea to readonly + this.shadow.querySelector('textarea').readOnly = false + } + } + + updateCharacterCount() { + const currentCharacterLength = this.shadow.querySelector('textarea').value.length + + if (this.maximumCharacterCount) { + const formattedCharacterLength = Number(currentCharacterLength).toLocaleString(); + const formattedMaxNumber = Number(this.maximumCharacterCount).toLocaleString(); + this.shadow.querySelector('#character-count').textContent = `${formattedCharacterLength}/${formattedMaxNumber}`; + } else { + this.shadow.querySelector('#character-count').textContent = ``; + } + + // If the character length exceeds the maximum character count, show the text in red + if (currentCharacterLength > this.maximumCharacterCount) { + this.shadow.querySelector('#character-count').style.color = "#F0384E"; + this.shadow.querySelector('#character-count').style.opacity = 1; + } else { + this.shadow.querySelector('#character-count').style.color = "var(--ob-text-color)"; + this.shadow.querySelector('#character-count').style.opacity = 0.5; + } + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-editor', OuterbasePluginEditor_$PLUGIN_ID) + +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-editor-$PLUGIN_ID', OuterbasePluginEditor_$PLUGIN_ID) \ No newline at end of file diff --git a/columns/hackathon/foreign-key.js b/columns/hackathon/foreign-key.js new file mode 100644 index 0000000..c76153e --- /dev/null +++ b/columns/hackathon/foreign-key.js @@ -0,0 +1,531 @@ +var privileges_$PLUGIN_ID = [ + "cellValue", + "rowValue", + "tableValue", + "tableSchemaValue", + "databaseSchemaValue", + "configuration", + "metadata" +] + +var OuterbaseEvent_$PLUGIN_ID = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent_$PLUGIN_ID = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + theme = "light" + preferredColumn = "" + + constructor(object) { + this.theme = object?.theme ? object.theme : "light"; + this.preferredColumn = object?.preferredColumn ? object.preferredColumn : ""; + } +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+
+
+ +
+ +
+
+
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + } + + storeValueInCache(key, value) { + const cacheName = 'pluginForeignKeyCache' + const currentCache = JSON.parse(localStorage.getItem(cacheName)) ?? {} + + if (currentCache[key] === value) { + return + } + + // Check how many keys are in the cache + const keys = Object.keys(currentCache) + + if (keys.length > 500) { + // Remove the first 100 keys + for (let i = 0; i < 100; i++) { + delete currentCache[keys[i]] + } + } + + currentCache[key] = value + localStorage.setItem(cacheName, JSON.stringify(currentCache)) + } + + getValueFromCache(key) { + const cacheName = 'pluginForeignKeyCache' + const currentCache = JSON.parse(localStorage.getItem(cacheName)) ?? {} + + return currentCache[key] + } + + async attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + // console.log('FK Cell Config: ', this.config) + // console.log('FK Cell Metadata: ', metadata) + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + + this.render() + } + + async render() { + let cellValue = this.getAttribute('cellvalue') + + if (cellValue.length === 0) { + cellValue = "NULL" + } + + this.shadow.querySelector('#label').innerText = cellValue + + try { + // Based on the information provided to our plugin, we need to identify + // what the column constraints are and what database table it is linked to. + // This will allow us to construct a SQL query to fetch the value from the + // linked table. + const column = this.getAttribute('columnName') + const table = JSON.parse(this.getAttribute('tableSchemaValue')).name + const schema = JSON.parse(this.getAttribute('tableSchemaValue')).schema ?? "public" + const databaseSchemaValue = JSON.parse(this.getAttribute('databaseSchemaValue')) + const schemaColumns = databaseSchemaValue?.[schema] + const schemaTable = schemaColumns.find(t => t.name === table) + const constraints = schemaTable?.constraints + + if (constraints.length === 0) { + this.shadow.querySelector('#loader').style.display = 'none' + return + } + + let fkName = "" + let fkTable = "" + let fkSchema = "" + let cellValue = this.getAttribute('cellValue') + + // Loop through `constraints` and find where type === `FOREIGN KEY` + for (const constraint of constraints) { + if (constraint.type === "FOREIGN KEY" && constraint.table === table && constraint.column === column) { + const fkColumn = constraint.columns?.[0] + fkName = fkColumn.name + fkTable = fkColumn.table + fkSchema = fkColumn.schema ?? "public" + } + } + + // Necessary information is graciously stored by Outerbase in the `localStorage` + // for us to make the necessary network request to fetch the value from the linked table. + const session = JSON.parse(localStorage.getItem('session')) + const workspaceId = localStorage.getItem('workspace_id') + const sourceId = localStorage.getItem('source_id') + + // Create a unique cache key based on the `schema.table.column.value` + const cacheKey = `${fkSchema}.${fkTable}.${fkName}.${cellValue}` + + // If the `cacheKey` already exists in the cache, use the cached value instead + // of making another network request to fetch it. + if (this.getValueFromCache(cacheKey)) { + this.shadow.querySelector('#label').innerText = this.getValueFromCache(cacheKey) + this.shadow.querySelector('#loader').style.display = 'none' + return + } + + // When a cached value does not exist for this `schema.table.column.value`, fetch the value + // from the database and store it in the cache for future re-use. + await fetch(`https://app.dev.outerbase.com/api/v1/workspace/${workspaceId}/source/${sourceId}/query/raw`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': session?.state?.session?.token + }, + body: JSON.stringify({ + query: `SELECT * FROM ${fkSchema}.${fkTable} WHERE ${fkName} = '${cellValue}'`, + options: {} + }) + }).then(response => response.json()).then(data => { + const item = data.response?.items?.[0] ?? {} + const bestCandidate = this.detectGoodColumnCandidate(item) + this.shadow.querySelector('#label').innerText = bestCandidate + + // Set cache + this.storeValueInCache(cacheKey, bestCandidate) + this.shadow.querySelector('#loader').style.display = 'none' + }) + } catch (error) { + console.error(error) + this.shadow.querySelector('#loader').style.display = 'none' + } + } + + detectGoodColumnCandidate(column) { + const preferredColumn = this.config.preferredColumn?.length > 0 ? column[this.config.preferredColumn] : null + let firstStringFound = Object.entries(column).find(([key, value]) => typeof value === 'string') + let bestMatch = null + let bestMatchEmail = null + let bestMatchPhone = null + let bestMatchAddress = null + + for (const [key, value] of Object.entries(column)) { + if (key.includes('name') && !bestMatch) { + bestMatch = value + } else if (key.includes('email') && !bestMatchEmail) { + bestMatchEmail = value + } else if (key.includes('phone') && !bestMatchPhone) { + bestMatchPhone = value + } else if (key.includes('address') && !bestMatchAddress) { + bestMatchAddress = value + } + } + + return preferredColumn ? preferredColumn : bestMatch ?? bestMatchEmail ?? bestMatchPhone ?? bestMatchAddress ?? firstStringFound + } +} + + + + + + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration = document.createElement("template") +templateConfiguration.innerHTML = ` + + +
+

Select Foreign Key Column

+ + + + +
+` + +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + // Clear FK cache + const cacheName = 'pluginForeignKeyCache' + localStorage.removeItem(cacheName) + + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseEvent_$PLUGIN_ID.onSave, + value: this.config + }) + }); + + // var saveButton = this.shadow.getElementById("saveButton"); + // saveButton.addEventListener("click", () => { + // triggerEvent(this, { + // action: OuterbaseEvent.onSave, + // value: this.config.toJSON() + // }) + // }); + + // Listen to when the select option changes and store the selected option + this.shadow.querySelector('select').addEventListener('change', (event) => { + this.config.preferredColumn = event.target.value + }) + + this.fetchConstraintMetadata() + this.render() + } + + async fetchConstraintMetadata() { + try { + // Based on the information provided to our plugin, we need to identify + // what the column constraints are and what database table it is linked to. + // This will allow us to construct a SQL query to fetch the value from the + // linked table. + const column = this.getAttribute('columnName') + const table = JSON.parse(this.getAttribute('tableSchemaValue')).name + const schema = JSON.parse(this.getAttribute('tableSchemaValue')).schema ?? "public" + const databaseSchemaValue = JSON.parse(this.getAttribute('databaseSchemaValue')) + const schemaColumns = databaseSchemaValue?.[schema] + const schemaTable = schemaColumns.find(t => t.name === table) + const constraints = schemaTable?.constraints + + // if (constraints.length === 0) { + // this.shadow.querySelector('#loader').style.display = 'none' + // return + // } + + let fkName = "" + let fkTable = "" + let fkSchema = "" + // let cellValue = this.getAttribute('cellValue') + + // Loop through `constraints` and find where type === `FOREIGN KEY` + for (const constraint of constraints) { + if (constraint.type === "FOREIGN KEY" && constraint.table === table && constraint.column === column) { + const fkColumn = constraint.columns?.[0] + fkName = fkColumn.name + fkTable = fkColumn.table + fkSchema = fkColumn.schema ?? "public" + } + } + + // Necessary information is graciously stored by Outerbase in the `localStorage` + // for us to make the necessary network request to fetch the value from the linked table. + const session = JSON.parse(localStorage.getItem('session')) + const workspaceId = localStorage.getItem('workspace_id') + const sourceId = localStorage.getItem('source_id') + + // Create a unique cache key based on the `schema.table.column.value` + // const cacheKey = `${fkSchema}.${fkTable}.${fkName}.${cellValue}` + + // // If the `cacheKey` already exists in the cache, use the cached value instead + // // of making another network request to fetch it. + // if (this.getValueFromCache(cacheKey)) { + // this.shadow.querySelector('#label').innerText = this.getValueFromCache(cacheKey) + // this.shadow.querySelector('#loader').style.display = 'none' + // return + // } + + // When a cached value does not exist for this `schema.table.column.value`, fetch the value + // from the database and store it in the cache for future re-use. + await fetch(`https://app.dev.outerbase.com/api/v1/workspace/${workspaceId}/source/${sourceId}/query/raw`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': session?.state?.session?.token + }, + body: JSON.stringify({ + query: `SELECT * FROM ${fkSchema}.${fkTable} LIMIT 1`, + options: {} + }) + }).then(response => response.json()).then(data => { + const item = data.response?.items?.[0] ?? {} + const keys = Object.keys(item) + // console.log('First Row Keys: ', keys) + + // Add a new `option` in the `select` for each keys object + let select = this.shadow.querySelector('select') + keys.forEach(key => { + let option = document.createElement('option') + option.value = key + option.text = key + select.appendChild(option) + }) + }) + } catch (error) { + console.error(error) + } + } + + render() { + + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-configuration', OuterbasePluginConfiguration_$PLUGIN_ID) + +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) diff --git a/columns/hackathon/image.js b/columns/hackathon/image.js new file mode 100644 index 0000000..1bff1a4 --- /dev/null +++ b/columns/hackathon/image.js @@ -0,0 +1,328 @@ +var observableAttributes = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var OuterbaseEvent = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + cellValue = undefined + + constructor(object) { + + } +} + +var triggerEvent = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ +
+ +
+

Overlay

+
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) + this.render() + + // this.shadow.querySelector("img").addEventListener("click", () => { + // // Make `overlay` visible + // this.shadow.querySelector("#overlay").style.opacity = 1; + // this.shadow.querySelector("#overlay").style.display = 'block'; + // }) + + var viewImageButton = this.shadow.querySelector("img"); + viewImageButton.addEventListener("click", () => { + let url = `${this.getAttribute('cellvalue')}` + window.open(url, '_blank') + }); + } + + render() { + this.shadow.querySelector("img").src = this.getAttribute('cellvalue') + } + + callCustomEvent(data) { + const event = new CustomEvent('custom-change', { + detail: data, + bubbles: true, // If you want the event to bubble up through the DOM + composed: true // Allows the event to pass through shadow DOM boundaries + }); + + this.dispatchEvent(event); + } +} + +/** + * **************** + * Cell Editor View + * **************** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * An optional view that pops below the cell for an expanded viewing area + * of additional UI data. + */ +var templateEditor_$PLUGIN_ID = document.createElement("template") +templateEditor_$PLUGIN_ID.innerHTML = ` + + +
+

Editor View

+
+` + +class OuterbasePluginCellEditor_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateEditor_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) + this.config.cellValue = decodeAttributeByName(this, "cellValue") + this.render() + } + + render() { + + } +} + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration = document.createElement("template") +templateConfiguration.innerHTML = ` + + +
+ +
+` + +// For Configuration view, let them optionally provide a PREFIX URL +// to attach to all URL's in the column. If none is provided, just +// try using the value of the cell. + +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName(this, "configuration")) + this.config.cellValue = decodeAttributeByName(this, "cellValue") + this.render() + } + + render() { + this.shadow.querySelector("#container").innerHTML = ` +
+

Hello, Configuration World!

+ +
+ ` + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + triggerEvent(this, { + action: OuterbaseEvent.onSave, + value: {} + }) + }); + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-cell-editor', OuterbasePluginCellEditor_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) diff --git a/columns/hackathon/link.js b/columns/hackathon/link.js new file mode 100644 index 0000000..f25a77a --- /dev/null +++ b/columns/hackathon/link.js @@ -0,0 +1,285 @@ +var observableAttributes_$PLUGIN_ID = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var OuterbaseEvent_$PLUGIN_ID = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent_$PLUGIN_ID = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + theme = "light" + baseURL = "" + + constructor(object) { + this.theme = object?.theme ? object.theme : "light"; + this.baseURL = object?.baseUrl ? object.baseUrl : ""; + } +} + +var triggerEvent = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ +
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + + // When a user clicks the span open a new tab with the link + this.shadow.querySelector('span').addEventListener('click', () => { + let url = `${this.config.baseURL}${this.getAttribute('cellvalue')}` + window.open(url, '_blank') + }) + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + + render() { + let cellValue = this.getAttribute('cellvalue') + + if (cellValue.length === 0) { + cellValue = "NULL" + } + + this.shadow.querySelector('span').innerText = cellValue + } +} + + + + + + +// For Configuration view, let them optionally provide a PREFIX URL +// to attach to all URL's in the column. If none is provided, just +// try using the value of the cell. + + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration = document.createElement("template") +templateConfiguration.innerHTML = ` + + +
+

Select URL Options

+ +
+
Base URL: (optional)
+ +
+ + Save View +
+` + +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + this.config.baseURL = this.shadow.getElementById("prefixValue").value + + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseEvent_$PLUGIN_ID.onSave, + value: this.config + }) + }); + + this.render() + } + + render() { + + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-configuration', OuterbasePluginConfiguration_$PLUGIN_ID) + + +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) diff --git a/columns/hackathon/privacy.js b/columns/hackathon/privacy.js new file mode 100644 index 0000000..3146926 --- /dev/null +++ b/columns/hackathon/privacy.js @@ -0,0 +1,170 @@ +var observableAttributes_$PLUGIN_ID = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var OuterbaseEvent = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + cellValue = undefined + + constructor(object) { + + } +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'") + ?.replace(/`/g, "`"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ +
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + + render() { + let cellValue = this.getAttribute('cellvalue') + + if (cellValue.length === 0) { + cellValue = "NULL" + } + + this.shadow.querySelector('input').value = cellValue + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) \ No newline at end of file diff --git a/columns/hackathon/value-range.js b/columns/hackathon/value-range.js new file mode 100644 index 0000000..99f275b --- /dev/null +++ b/columns/hackathon/value-range.js @@ -0,0 +1,547 @@ +var observableAttributes_$PLUGIN_ID = [ + // The value of the cell that the plugin is being rendered in + "cellValue", + // The configuration object that the user specified when installing the plugin + "configuration", + // Additional information about the view such as count, page and offset. + "metadata" +] + +var OuterbaseEvent_$PLUGIN_ID = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var OuterbaseColumnEvent_$PLUGIN_ID = { + // The user has began editing the selected cell + onEdit: "onEdit", + // Stops editing a cells editor popup view and accept the changes + onStopEdit: "onStopEdit", + // Stops editing a cells editor popup view and prevent persisting the changes + onCancelEdit: "onCancelEdit", + // Updates the cells value with the provided value + updateCell: "updateCell", +} + +/** + * ****************** + * Custom Definitions + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░▄▄████▄▄░░░░░ + * ░░░██████████░░░░ + * ░░░██▄▄██▄▄██░░░░ + * ░░░░▄▀▄▀▀▄▀▄░░░░░ + * ░░░▀░░░░░░░░▀░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * class for you to manage properties between the other classes below, however, it's strictly optional. + * However, this would be a good class to contain the properties you need to store when a user installs + * or configures your plugin. + */ +class OuterbasePluginConfig_$PLUGIN_ID { + theme = "light" + rangeOptions = [] + + constructor(object) { + this.theme = object?.theme ? object.theme : "light"; + this.rangeOptions = object?.rangeOptions ? object.rangeOptions : []; + } +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +/** + * ********** + * Cell View + * ********** + * + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░▄▄████▄▄░░░░░ + * ░░░▄██████████▄░░░ + * ░▄██▄██▄██▄██▄██▄░ + * ░░░▀█▀░░▀▀░░▀█▀░░░ + * ░░░░░░░░░░░░░░░░░░ + * ░░░░░░░░░░░░░░░░░░ + * + * TBD + */ +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+
+ +
+` + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.render() + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + + render() { + let cellValue = this.getAttribute('cellvalue') + + if (cellValue.length === 0) { + cellValue = "NULL" + } + + this.shadow.querySelector('span').innerText = cellValue + + // Get the indicator element + const indicator = this.shadow.querySelector("#indicator") + + if (cellValue === "NULL") { + indicator.style.backgroundColor = "transparent" + return + } + + let number = Number(cellValue) + let rangeOptions = this.config.rangeOptions + + for (let i = 0; i < rangeOptions.length; i++) { + let range = rangeOptions[i] + let value = Number(range.value) + + if (range.operator === ">") { + if (number > value) { + indicator.style.backgroundColor = range.color + return + } + } else if (range.operator === ">=") { + if (number >= value) { + indicator.style.backgroundColor = range.color + return + } + } else if (range.operator === "<") { + if (number < value) { + indicator.style.backgroundColor = range.color + return + } + } else if (range.operator === "<=") { + if (number <= value) { + indicator.style.backgroundColor = range.color + return + } + } else if (range.operator === "=") { + if (number === value) { + indicator.style.backgroundColor = range.color + return + } + } + } + + // If the indicator is less than 5, set the color to red + // let number = parseInt(cellValue) + // if (number <= 5) { + // indicator.style.backgroundColor = "#fafafa" + // } else if (number > 5 && number < 10) { + // indicator.style.backgroundColor = "#a8a29e" + // } else { + // indicator.style.backgroundColor = "#44403c" + // } + + } +} + + + + + + + +// SQL to get range of integer values: +// ---- +// SELECT MIN(column_name) AS MinValue, MAX(column_name) AS MaxValue +// FROM table_name; +// ---- +// Put the above in a configuration view so we can quickly sample the data +// and provide a range of values to the user for their behalf. +// May also allow them to put a MIN and MAX in as well, or define +// values to indicator colors themselves. + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration = document.createElement("template") +templateConfiguration.innerHTML = ` + + +
+

Select Range Options

+ +
+ Min Value: + + + Max Value: + + + +
+ +
+ +
+ + Add Range + + Save View +
+` + +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return observableAttributes_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + minValue = 0 + maxValue = 50 + rangeOptions = [ + // { color: "#FF0000", operator: ">", value: 5 }, + // { color: "#00FF00", operator: "<=", value: 5 } + ] + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + this.config.rangeOptions = this.rangeOptions + + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseEvent_$PLUGIN_ID.onSave, + value: this.config + }) + }); + + // When the `addRange` button is clicked add a new entry to this.rangeOptions + var addRangeButton = this.shadow.getElementById("addRange") + addRangeButton.addEventListener("click", () => { + this.rangeOptions.push({ color: "#000000", operator: ">", value: 0 }) + this.render() + }) + + // If user clicks `valueRangeApply` button, update the minValue and maxValue + var applyButton = this.shadow.getElementById("valueRangeApply") + applyButton.addEventListener("click", () => { + this.minValue = Number(this.shadow.getElementById("minValue").value) + this.maxValue = Number(this.shadow.getElementById("maxValue").value) + this.smartRangeLayout() + }) + + this.fetchMinMaxValues() + this.render() + } + + smartRangeLayout() { + console.log('Min: ', this.minValue) + console.log('Max: ', this.maxValue) + + // Based on the minValue and maxValue can we automatically create a range of values + // for the user to select from. Try to figure out how many values to create and add + // them to the `rangeOptions` array with default values. + const range = this.maxValue - this.minValue + const step = range / 5 + + const defaultColors = ['#f5f5f5', '#d4d4d4', '#a3a3a3', '#525252', '#262626'] + + this.rangeOptions = [] + + for (let i = 0; i < 5; i++) { + this.rangeOptions.push({ color: defaultColors[i], operator: ">", value: this.minValue + (step * i) }) + } + + // Revere the array + this.rangeOptions.reverse() + + this.render() + } + + async fetchMinMaxValues() { + // Testing locally with this + // this.minValue = 10 + // this.maxValue = 100 + // this.smartRangeLayout() + // return + + + try { + // Based on the information provided to our plugin, we need to identify + // what the column constraints are and what database table it is linked to. + // This will allow us to construct a SQL query to fetch the value from the + // linked table. + const column = this.getAttribute('columnName') + const table = JSON.parse(this.getAttribute('tableSchemaValue')).name + const schema = JSON.parse(this.getAttribute('tableSchemaValue')).schema ?? "public" + + // Necessary information is graciously stored by Outerbase in the `localStorage` + // for us to make the necessary network request to fetch the value from the linked table. + const session = JSON.parse(localStorage.getItem('session')) + const workspaceId = localStorage.getItem('workspace_id') + const sourceId = localStorage.getItem('source_id') + + // SELECT DISTINCT column_name FROM table_name; + + // When a cached value does not exist for this `schema.table.column.value`, fetch the value + // from the database and store it in the cache for future re-use. + // SELECT MIN(column_name) AS MinValue, MAX(column_name) AS MaxValue FROM table_name; + + await fetch(`https://app.dev.outerbase.com/api/v1/workspace/${workspaceId}/source/${sourceId}/query/raw`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': session?.state?.session?.token + }, + body: JSON.stringify({ + query: `SELECT MIN(${column}) AS minValue, MAX(${column}) AS maxValue FROM ${schema}.${table};`, + options: {} + }) + }).then(response => response.json()).then(data => { + const items = data.response?.items ?? [] + + if (items.length) { + const first = items[0] + this.minValue = Number(first.minValue) + this.maxValue = Number(first.maxValue) + } + + this.smartRangeLayout(); + }) + } catch (error) { + console.error(error) + } + } + + render() { + // Clear all the range definitions + const rangeDefinitions = this.shadow.getElementById("range-definitions") + rangeDefinitions.innerHTML = "" + + this.shadow.getElementById("minValue").value = this.minValue + this.shadow.getElementById("maxValue").value = this.maxValue + + // const rangeDefinitions = this.shadow.getElementById("range-definitions") + this.rangeOptions.forEach((item, index) => { + const rangeItem = this.createRangeItem(index, item.color, item.operator, item.value) + rangeDefinitions.appendChild(rangeItem) + }) + + // Detect a change in range item color field and update the indicator + const rangeItems = this.shadow.querySelectorAll(".range-option") + rangeItems.forEach(item => { + item.querySelector("input[type='text']").addEventListener("change", event => { + const indicator = item.querySelector(".indicator") + indicator.style.backgroundColor = event.target.value + + // Update this value in the `this.rangeOptions` array, get the index from the `data-item-id` attribute + const index = item.getAttribute("data-item-id") + if (!index) return + this.rangeOptions[index].color = event.target.value + }) + }) + + // Detect a change in range item operator field and update the indicator + rangeItems.forEach(item => { + item.querySelector("select").addEventListener("change", event => { + // Update this value in the `this.rangeOptions` array, get the index from the `data-item-id` attribute + const index = item.getAttribute("data-item-id") + if (!index) return + this.rangeOptions[index].operator = event.target.value + }) + }) + + // Detect a change in range item value field and update the indicator + rangeItems.forEach(item => { + item.querySelector("input[type='number']").addEventListener("change", event => { + // Update this value in the `this.rangeOptions` array, get the index from the `data-item-id` attribute + const index = item.getAttribute("data-item-id") + if (!index) return + this.rangeOptions[index].value = event.target.value + }) + }) + + // Detect a click on the remove button and remove the range item + rangeItems.forEach(item => { + item.querySelector("button").addEventListener("click", event => { + const index = item.getAttribute("data-item-id") + if (!index) return + this.rangeOptions.splice(index, 1) + this.render() + }) + }) + } + + createRangeItem(index, color, operator, value) { + const rangeItem = document.createElement("div") + + rangeItem.innerHTML = ` +
+
+ + + + +
+ ` + + return rangeItem + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +// window.customElements.define('outerbase-plugin-cell', OuterbasePluginCell_$PLUGIN_ID) +// window.customElements.define('outerbase-plugin-configuration', OuterbasePluginConfiguration_$PLUGIN_ID) + +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) diff --git a/columns/html-preview.js b/columns/html-preview.js new file mode 100644 index 0000000..1ff21f2 --- /dev/null +++ b/columns/html-preview.js @@ -0,0 +1,325 @@ +var privileges_$PLUGIN_ID = [ + 'cellValue', + 'configuration', +] + +var OuterbaseEvent_$PLUGIN_ID = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + +var templateCell_$PLUGIN_ID = document.createElement('template') +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ +
+ + + +
+
+` + +{/* */} + +var templateEditor_$PLUGIN_ID = document.createElement('template') +templateEditor_$PLUGIN_ID.innerHTML = ` + + +
+

+
+
+
+
+` + +// This is the configuration object that Outerbase passes to your plugin. +// Define all of the configuration options that your plugin requires here. +class OuterbasePluginConfig_$PLUGIN_ID { + theme = "light"; + + constructor(object) { + this.theme = object.theme ? object.theme : "light"; + } +} + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + // The shadow DOM is a separate DOM tree that is attached to the element. + // This allows us to encapsulate our styles and markup. It also prevents + // styles from the parent page from leaking into our plugin. + this.shadow = this.attachShadow({ mode: 'open' }) + this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + + // This function is called when the UI is made available into the DOM. Put any + // logic that you want to run when the element is first stood up here, such as + // event listeners, default values to display, etc. + connectedCallback() { + // Parse the configuration object from the `configuration` attribute + // and store it in the `config` property. + this.config = new OuterbasePluginConfig_$PLUGIN_ID( + JSON.parse(this.getAttribute('configuration')) + ) + + // Set default value based on input + this.shadow.querySelector('#html-value').value = this.getAttribute('cellvalue') + + var imageInput = this.shadow.getElementById("html-value"); + var viewImageButton = this.shadow.getElementById("action-button"); + + if (imageInput && viewImageButton) { + imageInput.addEventListener("focus", () => { + // Tell Outerbase to start editing the cell + this.callCustomEvent({ + action: 'onstopedit', + value: true + }) + }); + + imageInput.addEventListener("blur", () => { + // Tell Outerbase to update the cells raw value + this.callCustomEvent({ + action: 'cellvalue', + value: imageInput.value + }) + + // Then stop editing the cell and close the editor view + this.callCustomEvent({ + action: 'onstopedit', + value: true + }) + }); + + viewImageButton.addEventListener("click", () => { + this.callCustomEvent({ + action: 'onedit', + value: true + }) + }); + } + } + + callCustomEvent(data) { + const event = new CustomEvent('custom-change', { + detail: data, + bubbles: true, // If you want the event to bubble up through the DOM + composed: true // Allows the event to pass through shadow DOM boundaries + }); + + this.dispatchEvent(event); + } +} + +class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges + } + + static NO_VALUE_HEADER = "Nothing to preview"; + static BAD_VALUE_HEADER = "Failed to load preview"; + + config = new OuterbasePluginConfig_$PLUGIN_ID({}); + + constructor() { + super() + + // The shadow DOM is a separate DOM tree that is attached to the element. + // This allows us to encapsulate our styles and markup. It also prevents + // styles from the parent page from leaking into our plugin. + this.shadow = this.attachShadow({ mode: 'open' }) + this.shadow.appendChild(templateEditor_$PLUGIN_ID.content.cloneNode(true)) + + // Parse the configuration object from the `configuration` attribute + // and store it in the `config` property. + this.config = new OuterbasePluginConfig_$PLUGIN_ID( + JSON.parse(this.getAttribute('configuration')) + ) + } + + // This function is called when the UI is made available into the DOM. Put any + // logic that you want to run when the element is first stood up here, such as + // event listeners, default values to display, etc. + connectedCallback() { + const value = this.getAttribute("cellValue"); + + this.shadow.querySelector("#container").style.padding = "0px"; + this.shadow.querySelector("#header").style.display = "block"; + this.shadow.querySelector("#hr").style.display = "block"; + + if (this.isEmpty(value)) { + this.shadow.querySelector("#header").innerHTML = OuterbasePluginEditor_$PLUGIN_ID.NO_VALUE_HEADER; + return; + } + + var error = this.isInvalidHTML(value); + if (error) { + this.shadow.querySelector("#container").style.padding = "20px"; + this.shadow.querySelector("#header").innerHTML = OuterbasePluginEditor_$PLUGIN_ID.BAD_VALUE_HEADER; + this.shadow.querySelector("#error").style.display = "block"; + this.shadow.querySelector("#error").innerHTML = error.innerHTML; + this.shadow.querySelector("#content").innerText = value; + return; + } + + this.shadow.querySelector("#header").style.display = "none"; + this.shadow.querySelector("#hr").style.display = "none"; + this.shadow.querySelector("#content").innerHTML = value; + } + + isEmpty(data) { + return !data || data.length == 0; + } + + isInvalidHTML(data) { + const parser = new DOMParser(); + const doc = parser.parseFromString(data, "application/xml"); + return doc.querySelector("parsererror"); + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-editor-$PLUGIN_ID', OuterbasePluginEditor_$PLUGIN_ID) diff --git a/columns/image-viewer.js b/columns/image-viewer.js index 9abd19c..fb5fa42 100644 --- a/columns/image-viewer.js +++ b/columns/image-viewer.js @@ -1,8 +1,31 @@ -var privileges = [ +var privileges_$PLUGIN_ID = [ 'cellValue', 'configuration', ] +var OuterbaseEvent_$PLUGIN_ID = { + // The user has triggered an action to save updates + onSave: "onSave", +} + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("custom-change", { + detail: data, + bubbles: true, + composed: true + }); + + fromClass.dispatchEvent(event); +} + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +} + var templateCell_$PLUGIN_ID = document.createElement('template') templateCell_$PLUGIN_ID.innerHTML = ` -
+
- +
+ + + +
` @@ -42,7 +102,12 @@ var templateEditor_$PLUGIN_ID = document.createElement('template') templateEditor_$PLUGIN_ID.innerHTML = `
+ +
+ +
` // This is the configuration object that Outerbase passes to your plugin. // Define all of the configuration options that your plugin requires here. class OuterbasePluginConfig_$PLUGIN_ID { + baseURL = "" + theme = "light" + constructor(object) { - // No custom properties needed in this plugin. + this.baseURL = object?.baseUrl ?? "" + this.theme = object?.theme ? object.theme : "light"; } } class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { static get observedAttributes() { - return privileges + return privileges_$PLUGIN_ID } config = new OuterbasePluginConfig_$PLUGIN_ID({}) @@ -94,6 +206,17 @@ class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true)) } + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + var element = this.shadow.querySelector(".theme-container") + element.classList.remove("dark") + element.classList.add(this.config.theme); + } + // This function is called when the UI is made available into the DOM. Put any // logic that you want to run when the element is first stood up here, such as // event listeners, default values to display, etc. @@ -108,7 +231,7 @@ class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { this.shadow.querySelector('#image-value').value = this.getAttribute('cellvalue') var imageInput = this.shadow.getElementById("image-value"); - var viewImageButton = this.shadow.getElementById("view-image"); + var viewImageButton = this.shadow.getElementById("action-button"); if (imageInput && viewImageButton) { imageInput.addEventListener("focus", () => { @@ -182,10 +305,185 @@ class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { var backgroundImageView = this.shadow.getElementById("background-image"); if (imageView && backgroundImageView) { - imageView.src = this.getAttribute('cellvalue') - backgroundImageView.style.backgroundImage = `url(${this.getAttribute('cellvalue')})` + const source = this.config.baseURL ? `${this.config.baseURL}${this.getAttribute('cellvalue')}` : this.getAttribute('cellvalue') + imageView.src = source + backgroundImageView.style.backgroundImage = `url(${source})` + + this.getImageSize(source).then(size => { + const sizeInKilobytes = this.bytesToKilobytes(size); + + // Update image details + this.shadow.getElementById("image-details").innerHTML = ` +
${this.extractImageName(source)}
+
+
${sizeInKilobytes.toFixed(1)} KB
+ ` + this.shadow.getElementById("image-details").style.display = "flex"; + }); } } + + getImageSize(url) { + return fetch(url, { method: 'GET' }) // Use HEAD request to get headers without downloading the whole image + .then(response => { + const contentLength = response.headers.get('content-length'); + if (contentLength) { + return parseInt(contentLength, 10); + } else { + // If content-length header is not available, fetch the whole image and compute its size + return response.blob().then(data => data.size); + } + }).catch(() => { + // If the request fails, return 0 + return 0; + }); + } + + extractImageName(url) { + return url.split('/').pop(); + } + + bytesToKilobytes(bytes) { + return bytes / 1024; + } +} + + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration_$PLUGIN_ID = document.createElement("template") +var templateConfigurationInnerHTML_$PLUGIN_ID = ` + + +
+ +
+` + +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges_$PLUGIN_ID + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}) + + constructor() { + super() + + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration_$PLUGIN_ID.content.cloneNode(true)) + } + + connectedCallback() { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.cellValue = decodeAttributeByName_$PLUGIN_ID(this, "cellValue") + this.render() + } + + setupCheck() { + // Remove all elements from shadow DOM + while (this.shadow.firstChild) { + this.shadow.removeChild(this.shadow.firstChild); + } + + let template = templateConfiguration_$PLUGIN_ID + template.innerHTML = templateConfigurationInnerHTML_$PLUGIN_ID + this.shadow.appendChild(template.content.cloneNode(true)) + } + + render() { + this.setupCheck() + + this.shadow.querySelector("#container").innerHTML = ` +
+
+ If the URL values in this column require a base URL, enter it below. + This will be prepended to all URLs in this column. +
+ +
Base URL (optional):
+ + +
+ +
+
+ ` + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseEvent_$PLUGIN_ID.onSave, + value: { + baseURL: this.shadow.getElementById("base-url").value + } + }) + }); + } } // DO NOT change the name of this variable or the classes defined in this file. @@ -193,3 +491,4 @@ class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { // when installed in Outerbase. window.customElements.define('outerbase-plugin-cell-$PLUGIN_ID', OuterbasePluginCell_$PLUGIN_ID) window.customElements.define('outerbase-plugin-editor-$PLUGIN_ID', OuterbasePluginEditor_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) diff --git a/columns/json-universe.js b/columns/json-universe.js new file mode 100644 index 0000000..052e338 --- /dev/null +++ b/columns/json-universe.js @@ -0,0 +1,299 @@ +var privileges_$PLUGIN_ID = ["cellValue", "configuration", "metadata"]; + +var OuterbaseEvent_$PLUGIN_ID = { + // The user has triggered an action to save updates + onSave: "onSave", +}; + +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { + const event = new CustomEvent("change", { + detail: data, + bubbles: true, + composed: true, + }); + + fromClass.dispatchEvent(event); +}; + +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { + const encodedJSON = fromClass.getAttribute(name); + const decodedJSON = encodedJSON + ?.replace(/"/g, '"') + ?.replace(/'/g, "'"); + return decodedJSON ? JSON.parse(decodedJSON) : {}; +}; + +var templateCell_$PLUGIN_ID = document.createElement("template"); +templateCell_$PLUGIN_ID.innerHTML = ` + + +
+ + +
+ +
+
+`; + +var templateEditor_$PLUGIN_ID = document.createElement("template"); +templateEditor_$PLUGIN_ID.innerHTML = ` + + +
+ +
+`; + +/** + * 1. Use `` in the plugin directly + * 2. Listen for the `event.detail.code` change on each keystroke + * 3. Send the change to Starboard to update the cell value + * 4. Pass in starboard + */ + +// This is the configuration object that Outerbase passes to your plugin. +// Define all of the configuration options that your plugin requires here. +class OuterbasePluginConfig_$PLUGIN_ID { + baseURL = ""; + theme = "light"; + + constructor(object) { + this.baseURL = object?.baseUrl ?? ""; + this.theme = object?.theme ? object.theme : "light"; + } +} + +class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges_$PLUGIN_ID; + } + + config = new OuterbasePluginConfig_$PLUGIN_ID({}); + + constructor() { + super(); + + this.attributeChangedCallback = this.attributeChangedCallback.bind(this); + this.connectedCallback = this.connectedCallback.bind(this); + this.callCustomEvent = this.callCustomEvent.bind(this); + + // The shadow DOM is a separate DOM tree that is attached to the element. + // This allows us to encapsulate our styles and markup. It also prevents + // styles from the parent page from leaking into our plugin. + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild( + templateCell_$PLUGIN_ID.content.cloneNode(true) + ); + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID( + decodeAttributeByName_$PLUGIN_ID(this, "configuration") + ); + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata"); + this.config.theme = metadata?.theme; + + var element = this.shadowRoot.querySelector(".theme-container"); + element.classList.remove("dark"); + element.classList.add(this.config.theme); + } + + // This function is called when the UI is made available into the DOM. Put any + // logic that you want to run when the element is first stood up here, such as + // event listeners, default values to display, etc. + connectedCallback() { + // Parse the configuration object from the `configuration` attribute + // and store it in the `config` property. + this.config = new OuterbasePluginConfig_$PLUGIN_ID( + JSON.parse(this.getAttribute("configuration")) + ); + + // Set default value based on input + this.shadowRoot.querySelector("#image-value").value = + this.getAttribute("cellvalue"); + + var imageInput = this.shadowRoot.getElementById("image-value"); + var viewImageButton = this.shadowRoot.getElementById("action-button"); + + if (imageInput && viewImageButton) { + const emit = this.callCustomEvent.bind(this); + // TODO This listener should be removed? + viewImageButton.addEventListener("click", () => { + emit({ + action: "onedit", + value: true, + }); + }); + } + } + + callCustomEvent(data) { + const event = new CustomEvent("plugin-change", { + detail: data, + bubbles: true, // If you want the event to bubble up through the DOM + composed: true, // Allows the event to pass through shadow DOM boundaries + }); + + this.dispatchEvent(event); + } +} + +class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement { + static get observedAttributes() { + return privileges_$PLUGIN_ID; + } + + constructor() { + super(); + + // this.attributeChangedCallback = this.attributeChangedCallback.bind(this); + this.connectedCallback = this.connectedCallback.bind(this); + + // The shadow DOM is a separate DOM tree that is attached to the element. + // This allows us to encapsulate our styles and markup. It also prevents + // styles from the parent page from leaking into our plugin. + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild( + templateEditor_$PLUGIN_ID.content.cloneNode(true) + ); + + // Parse the configuration object from the `configuration` attribute + // and store it in the `config` property. + this.config = new OuterbasePluginConfig_$PLUGIN_ID( + JSON.parse(this.getAttribute("configuration")) + ); + } + + // This function is called when the UI is made available into the DOM. Put any + // logic that you want to run when the element is first stood up here, such as + // event listeners, default values to display, etc. + connectedCallback() { + if (this.config.theme === "light") { + this.shadowRoot.querySelector("#container").style.backgroundColor = + "white"; + } else { + this.shadowRoot.querySelector("#container").style.backgroundColor = + "black"; + } + + this.editor = this.shadowRoot.getElementById("editor"); + this.editor.setAttribute("code", this.getAttribute("cellvalue")); + this.editor.addEventListener("editor-change", (event) => { + event.stopPropagation(); + const { + detail: { value: jsonString }, + } = event; + + try { + const value = JSON.parse(jsonString); + this.dispatchEvent( + new CustomEvent("plugin-change", { + bubbles: true, + composed: true, + detail: { + action: "updatecell", + value, + }, + }) + ); + } catch (err) { + console.error(err); + } + }); + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.theme = metadata?.theme + + // Set the `mode` attribute of id `editor` to value of `theme` + var editor = this.shadowRoot.getElementById("editor") + if (editor) { + editor.setAttribute("mode", this.config.theme) + } + } +} + +// DO NOT change the name of this variable or the classes defined in this file. +// Changing the name of this variable will cause your plugin to not work properly +// when installed in Outerbase. +window.customElements.define( + "outerbase-plugin-cell-$PLUGIN_ID", + OuterbasePluginCell_$PLUGIN_ID +); +window.customElements.define( + "outerbase-plugin-editor-$PLUGIN_ID", + OuterbasePluginEditor_$PLUGIN_ID +); diff --git a/index.html b/index.html index 9b251d8..5c6792e 100644 --- a/index.html +++ b/index.html @@ -189,7 +189,10 @@
diff --git a/index2.html b/index2.html new file mode 100644 index 0000000..64b942b --- /dev/null +++ b/index2.html @@ -0,0 +1,80 @@ + + + Plugin Playground + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/tables/dealership.js b/tables/dealership.js index 5b7f50e..e75dc25 100644 --- a/tables/dealership.js +++ b/tables/dealership.js @@ -1,4 +1,4 @@ -var observableAttributes = [ +var observableAttributes_$PLUGIN_ID = [ // The value of the cell that the plugin is being rendered in "cellvalue", // The value of the row that the plugin is being rendered in @@ -15,14 +15,14 @@ var observableAttributes = [ "metadata" ] -var OuterbaseEvent = { +var OuterbaseEvent_$PLUGIN_ID = { // The user has triggered an action to save updates onSave: "onSave", // The user has triggered an action to configure the plugin configurePlugin: "configurePlugin", } -var OuterbaseColumnEvent = { +var OuterbaseColumnEvent_$PLUGIN_ID = { // The user has began editing the selected cell onEdit: "onEdit", // Stops editing a cells editor popup view and accept the changes @@ -33,7 +33,7 @@ var OuterbaseColumnEvent = { updateCell: "updateCell", } -var OuterbaseTableEvent = { +var OuterbaseTableEvent_$PLUGIN_ID = { // Updates the value of a row with the provided JSON value updateRow: "updateRow", // Deletes an entire row with the provided JSON value @@ -59,12 +59,12 @@ var OuterbaseTableEvent = { * ░░░▀░░░░░░░░▀░░░░ * ░░░░░░░░░░░░░░░░░ * - * Define your custom classes here. We do recommend the usage of our `OuterbasePluginConfig_$PLUGIN_ID` + * Define your custom classes here. We do recommend the usage of our `OuterbasePluginModel_$PLUGIN_ID` * class for you to manage properties between the other classes below, however, it's strictly optional. * However, this would be a good class to contain the properties you need to store when a user installs * or configures your plugin. */ -class OuterbasePluginConfig_$PLUGIN_ID { +class OuterbasePluginModel_$PLUGIN_ID { // Inputs from Outerbase for us to retain tableValue = undefined count = 0 @@ -103,7 +103,7 @@ class OuterbasePluginConfig_$PLUGIN_ID { } } -var triggerEvent = (fromClass, data) => { +var triggerEvent_$PLUGIN_ID = (fromClass, data) => { const event = new CustomEvent("custom-change", { detail: data, bubbles: true, @@ -113,7 +113,7 @@ var triggerEvent = (fromClass, data) => { fromClass.dispatchEvent(event); } -var decodeAttributeByName = (fromClass, name) => { +var decodeAttributeByName_$PLUGIN_ID = (fromClass, name) => { const encodedJSON = fromClass.getAttribute(name); const decodedJSON = encodedJSON ?.replace(/"/g, '"') @@ -135,8 +135,8 @@ var decodeAttributeByName = (fromClass, name) => { * ░░░░░░░░░░░░░░░░░░ * ░░░░░░░░░░░░░░░░░░ */ -var templateTable = document.createElement("template") -templateTable.innerHTML = ` +var templateTable_$PLUGIN_ID = document.createElement("template") +var templateTableInnerHTML_$PLUGIN_ID = ` - -
- -
-` -class OuterbasePluginConfig_$PLUGIN_ID { - imageKey = undefined - titleKey = undefined - descriptionKey = undefined - subtitleKey = undefined + @media only screen and (min-width: 768px) { + .grid-container { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 32px; + } + } - constructor(object) { - this.imageKey = object?.imageKey - this.titleKey = object?.titleKey - this.descriptionKey = object?.descriptionKey - this.subtitleKey = object?.subtitleKey + @media only screen and (min-width: 1200px) { + .grid-container { + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 32px; + } } - toJSON() { - return { - "imageKey": this.imageKey, - "titleKey": this.titleKey, - "descriptionKey": this.descriptionKey, - "subtitleKey": this.subtitleKey + @media only screen and (min-width: 1600px) { + .grid-container { + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 32px; } } -} + + +
+
+ +
+
+` +// Can the above div just be a self closing container:
class OuterbasePluginTable_$PLUGIN_ID extends HTMLElement { static get observedAttributes() { - return privileges + return observableAttributes_$PLUGIN_ID } config = new OuterbasePluginConfig_$PLUGIN_ID({}) - items = [] constructor() { super() - this.shadow = this.attachShadow({ mode: 'open' }) - this.shadow.appendChild(templateTable.content.cloneNode(true)) + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateTable_$PLUGIN_ID.content.cloneNode(true)) } connectedCallback() { - const encodedTableJSON = this.getAttribute('configuration'); - const decodedTableJSON = encodedTableJSON - ?.replace(/"/g, '"') - ?.replace(/'/g, "'"); - const configuration = JSON.parse(decodedTableJSON); - - if (configuration) { - this.config = new OuterbasePluginConfig_$PLUGIN_ID( - configuration - ) - } + this.render() + } - // Set the items property to the value of the `tableValue` attribute. - if (this.getAttribute('tableValue')) { - const encodedTableJSON = this.getAttribute('tableValue'); - const decodedTableJSON = encodedTableJSON - ?.replace(/"/g, '"') - ?.replace(/'/g, "'"); - this.items = JSON.parse(decodedTableJSON); - } + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.tableValue = decodeAttributeByName_$PLUGIN_ID(this, "tableValue") + + let metadata = decodeAttributeByName_$PLUGIN_ID(this, "metadata") + this.config.count = metadata?.count + this.config.limit = metadata?.limit + this.config.offset = metadata?.offset + this.config.theme = metadata?.theme + this.config.page = metadata?.page + this.config.pageCount = metadata?.pageCount + + var element = this.shadow.getElementById("theme-container"); + element.classList.remove("dark") + element.classList.add(this.config.theme); - // Manually render dynamic content this.render() } render() { - this.shadow.querySelector('#container').innerHTML = ` + this.shadow.querySelector("#container").innerHTML = `
- ${this.items.map((item) => ` + ${this.config?.tableValue?.length && this.config?.tableValue?.map((row) => `
- ${ this.config.imageKey ? `
` : `` } + ${ (this.config.imageKey && this.isValidURL(`${this.config.optionalImagePrefix ?? ''}${row[this.config.imageKey]}`)) + ? `
` + : `
+
+
+
No image selected
+ +
+
+
` }
- ${ this.config.titleKey ? `

${item[this.config.titleKey]}

` : `` } - ${ this.config.descriptionKey ? `

${item[this.config.descriptionKey]}

` : `` } - ${ this.config.subtitleKey ? `

${item[this.config.subtitleKey]}

` : `` } + ${ this.config.titleKey ? `

${row[this.config.titleKey]}

` : `` } + ${ this.config.subtitleKey ? `

${row[this.config.subtitleKey]}

` : `` } + ${ this.config.descriptionKey ? `

${row[this.config.descriptionKey]}

` : `` }
`).join("")}
+ +
+ Viewing ${this.config.offset} - ${this.config.limit} of ${this.config.count} results +
+ Page ${this.config.page} of ${this.config.pageCount} +
+ ${this.config.page > 1 ? `` : ``} + ${this.config.page < this.config.pageCount ? `` : ``} +
` + + const configurePluginButtons = this.shadow.querySelectorAll('.select-column-link'); + configurePluginButtons.forEach((btn, index) => { + btn.addEventListener('click', () => { + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseEvent_$PLUGIN_ID.configurePlugin + }) + }); + }); + + var previousPageButton = this.shadow.getElementById("previousPageButton"); + previousPageButton?.addEventListener("click", () => { + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseTableEvent_$PLUGIN_ID.getPreviousPage, + value: {} + }) + }); + + var nextPageButton = this.shadow.getElementById("nextPageButton"); + nextPageButton?.addEventListener("click", () => { + triggerEvent_$PLUGIN_ID(this, { + action: OuterbaseTableEvent_$PLUGIN_ID.getNextPage, + value: {} + }) + }); + } + + isValidURL(string) { + try { + new URL(string); + } catch (_) { + return false; + } + + return true; } } -var templateConfiguration = document.createElement('template') -templateConfiguration.innerHTML = ` + +/** + * ****************** + * Configuration View + * ****************** + * + * ░░░░░░░░░░░░░░░░░ + * ░░░░░▀▄░░░▄▀░░░░░ + * ░░░░▄█▀███▀█▄░░░░ + * ░░░█▀███████▀█░░░ + * ░░░█░█▀▀▀▀▀█░█░░░ + * ░░░░░░▀▀░▀▀░░░░░░ + * ░░░░░░░░░░░░░░░░░ + * + * When a user either installs a plugin onto a table resource for the first time + * or they configure an existing installation, this is the view that is presented + * to the user. For many plugin applications it's essential to capture information + * that is required to allow your plugin to work correctly and this is the best + * place to do it. + * + * It is a requirement that a save button that triggers the `OuterbaseEvent.onSave` + * event exists so Outerbase can complete the installation or preference update + * action. + */ +var templateConfiguration_$PLUGIN_ID = document.createElement("template") +templateConfiguration_$PLUGIN_ID.innerHTML = ` -
- +
+
+ +
` +// Can the above div just be a self closing container:
-class OuterbasePluginTableConfiguration_$PLUGIN_ID extends HTMLElement { +class OuterbasePluginConfiguration_$PLUGIN_ID extends HTMLElement { static get observedAttributes() { - return privileges + return observableAttributes_$PLUGIN_ID } config = new OuterbasePluginConfig_$PLUGIN_ID({}) - items = [] constructor() { super() - // The shadow DOM is a separate DOM tree that is attached to the element. - // This allows us to encapsulate our styles and markup. It also prevents - // styles from the parent page from leaking into our plugin. - this.shadow = this.attachShadow({ mode: 'open' }) - this.shadow.appendChild(templateConfiguration.content.cloneNode(true)) + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.appendChild(templateConfiguration_$PLUGIN_ID.content.cloneNode(true)) } connectedCallback() { - // Parse the configuration object from the `configuration` attribute - // and store it in the `config` property. - const encodedTableJSON = this.getAttribute('configuration'); - const decodedTableJSON = encodedTableJSON - ?.replace(/"/g, '"') - ?.replace(/'/g, "'"); - const configuration = JSON.parse(decodedTableJSON); - - this.config = new OuterbasePluginConfig_$PLUGIN_ID( - configuration - ) - - // Set the items property to the value of the `tableValue` attribute. - if (this.getAttribute('tableValue')) { - const encodedTableJSON = this.getAttribute('tableValue'); - const decodedTableJSON = encodedTableJSON - ?.replace(/"/g, '"') - ?.replace(/'/g, "'"); - this.items = JSON.parse(decodedTableJSON); - } + this.render() + } + + attributeChangedCallback(name, oldValue, newValue) { + this.config = new OuterbasePluginConfig_$PLUGIN_ID(decodeAttributeByName_$PLUGIN_ID(this, "configuration")) + this.config.tableValue = decodeAttributeByName_$PLUGIN_ID(this, "tableValue") + this.config.theme = decodeAttributeByName_$PLUGIN_ID(this, "metadata").theme + + var element = this.shadow.getElementById("theme-container"); + element.classList.remove("dark") + element.classList.add(this.config.theme); - // Manually render dynamic content this.render() } render() { - let sample = this.items.length ? this.items[0] : {} + let sample = this.config.tableValue.length ? this.config.tableValue[0] : {} let keys = Object.keys(sample) - this.shadow.querySelector('#container').innerHTML = ` + if (!keys || keys.length === 0 || !this.shadow.querySelector('#configuration-container')) return + + this.shadow.querySelector('#configuration-container').innerHTML = `

Image Key

+

Image URL Prefix (optional)

+ +

Title Key

+ +

Longitude Key

+ + +

Latitude Key

+ + +

Image Key

+ + +

Title Key

+ + +

Subtitle Key

+ + +

Description Key

+ + +
+ +
+
+ +
+
+ + +
+

${sample[this.config.titleKey]}

+

${sample[this.config.descriptionKey]}

+

${sample[this.config.subtitleKey]}

+
+
+
+ ` + + var saveButton = this.shadow.getElementById("saveButton"); + saveButton.addEventListener("click", () => { + triggerEvent(this, { + action: OuterbaseEvent.onSave, + value: this.config.toJSON() + }) + }); + + var apiKeyInput = this.shadow.getElementById("apiKeyInput"); + apiKeyInput.addEventListener("change", () => { + this.config.apiKey = apiKeyInput.value + this.render() + }); + + var imageKeySelect = this.shadow.getElementById("imageKeySelect"); + imageKeySelect.addEventListener("change", () => { + this.config.imageKey = imageKeySelect.value + this.render() + }); + + var titleKeySelect = this.shadow.getElementById("titleKeySelect"); + titleKeySelect.addEventListener("change", () => { + this.config.titleKey = titleKeySelect.value + this.render() + }); + + var descriptionKeySelect = this.shadow.getElementById("descriptionKeySelect"); + descriptionKeySelect.addEventListener("change", () => { + this.config.descriptionKey = descriptionKeySelect.value + this.render() + }); + + var subtitleKeySelect = this.shadow.getElementById("subtitleKeySelect"); + subtitleKeySelect.addEventListener("change", () => { + this.config.subtitleKey = subtitleKeySelect.value + this.render() + }); + + var latitudeKeySelect = this.shadow.getElementById("latitudeKeySelect"); + latitudeKeySelect.addEventListener("change", () => { + this.config.latitudeKey = latitudeKeySelect.value + this.render() + }); + + var longitudeKeySelect = this.shadow.getElementById("longitudeKeySelect"); + longitudeKeySelect.addEventListener("change", () => { + this.config.longitudeKey = longitudeKeySelect.value + this.render() + }); + } +} + +window.customElements.define('outerbase-plugin-table-$PLUGIN_ID', OuterbasePluginTable_$PLUGIN_ID) +window.customElements.define('outerbase-plugin-configuration-$PLUGIN_ID', OuterbasePluginConfiguration_$PLUGIN_ID) \ No newline at end of file