UNPKG

@silexlabs/grapesjs-data-source

Version:
1,357 lines (1,273 loc) 55.6 kB
import { createRef, Ref, ref } from 'lit/directives/ref.js' import {repeat} from 'lit/directives/repeat.js' import GraphQL, { GraphQLOptions, GraphQLBackendType, LightweightType } from '../datasources/GraphQL' import { COMMAND_ADD_DATA_SOURCE, DATA_SOURCE_CHANGED, DATA_SOURCE_DATA_LOAD_END, DATA_SOURCE_ERROR, DATA_SOURCE_READY, DataSourceEditorViewOptions, IDataSource } from '../types' import { getDefaultOptions, getElementFromOption } from '../utils' import { getAllDataSources, addDataSource, removeDataSource } from '../model/dataSourceRegistry' import { css, html, LitElement, render, TemplateResult } from 'lit' import { property, state } from 'lit/decorators.js' import { Editor } from 'grapesjs' const COMMON_STYLES = css` :host { font-family: var(--gjs-main-font); font-size: var(--gjs-font-size); } .ds-field { padding: 10px; } .ds-field > span { display: block; } hr.ds-separator { border: none; border-top: 1px solid var(--ds-button-bg); } .ds-field input, .ds-field select { -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: var(--gjs-main-dark-color); border: none; box-shadow: none; border-radius: 2px; box-sizing: border-box; padding: 0; position: relative; padding: 10px; color: inherit; width: 100%; } .ds-btn-prim { color:inherit; background-color:var(--gjs-main-light-color); border-radius:2px; padding:3px 6px; padding:var(--gjs-input-padding); cursor:pointer; border:none } .ds-btn-prim:active { background-color:var(--gjs-main-light-color) } .ds-btn-danger { color: var(--gjs-light-color); background-color: transparent; } .ds-btn-danger:hover { color: var(--ds-highlight); } [disabled], [readonly] { font-style: italic; } /* Modal specific styles */ :host(.ds-modal) { max-width: 600px; width: 90vw; } ` export default (editor: Editor, options: Partial<DataSourceEditorViewOptions> = {}) => { // Settings dialog if (options.settingsEl) { // Get the container element for the UI const settingsEl = getElementFromOption(options.settingsEl, 'options.settingsEl') const dsSettings: Ref<SettingsDataSources> = createRef() editor.on(`${DATA_SOURCE_CHANGED} ${DATA_SOURCE_ERROR} ${DATA_SOURCE_READY} ${DATA_SOURCE_DATA_LOAD_END}`, () => { if(dsSettings.value) { dsSettings.value.dataSources = [...getAllDataSources()] dsSettings.value.requestUpdate() } else { renderSettings(editor, dsSettings, settingsEl) } }) renderSettings(editor, dsSettings, settingsEl) // Add a data source editor.Commands.add(COMMAND_ADD_DATA_SOURCE, { run() { dsSettings.value?.openDataSourceModal() }, }) } } function renderSettings(editor: Editor, dsSettings: Ref, settingsEl: HTMLElement) { render(html` <ds-settings ${ref(dsSettings)} .dataSources=${[]} .editor=${editor} @add=${(e: CustomEvent) => { const ds = e.detail as GraphQLOptions const newDS = new GraphQL(ds) addDataSource(newDS) }} @add-top=${(e: CustomEvent) => { const ds = e.detail as GraphQLOptions const newDS = new GraphQL(ds) addDataSource(newDS) }} @change=${() => { // Refresh the datasources list when a datasource is edited if (dsSettings.value) { const settings = dsSettings.value as SettingsDataSources settings.dataSources = [...getAllDataSources()] settings.requestUpdate() } }} @delete=${(e: CustomEvent) => { const ds = e.detail as IDataSource removeDataSource(ds) }} ></ds-settings> `, settingsEl) } /** * <ds-settings> is a web component that renders the settings dialog for the DataSourceEditor plugin */ class SettingsDataSources extends LitElement { @property({ type: Array }) dataSources: IDataSource[] @property({ type: Object }) editor: Editor | null @state() private updateCounter: number = 0 constructor() { super() this.dataSources = [] this.editor = null } connectedCallback() { super.connectedCallback() } openDataSourceModal(dataSource: IDataSource | null = null) { if (!this.editor) return const isEdit = !!dataSource const title = isEdit ? 'Edit Data Source' : 'Add Data Source' // Create or use existing data source const ds = dataSource || new GraphQL(getDefaultOptions(this.dataSources.length.toString())) // Create the form element const formElement = document.createElement('ds-settings__data-source') as SettingsDataSource formElement.dataSource = ds formElement.isEdit = isEdit // Handle form events const handleSave = () => { if (isEdit) { // Force new array reference and increment counter to trigger card re-renders this.dataSources = [...this.dataSources] this.updateCounter++ this.dispatchEvent(new CustomEvent('change', { detail: ds })) } else { this.dispatchEvent(new CustomEvent('add', { detail: ds })) } this.requestUpdate() this.editor!.Modal.close() } const handleDelete = () => { this.dispatchEvent(new CustomEvent('delete', { detail: ds })) this.editor!.Modal.close() } const handleCancel = () => { this.editor!.Modal.close() } const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { this.editor!.Modal.close() } } formElement.addEventListener('change', handleSave) formElement.addEventListener('cancel', handleCancel) if (isEdit) { formElement.addEventListener('delete', handleDelete) } // Add ESC key handler document.addEventListener('keydown', handleKeyDown) // Clean up on modal close const modalObserver = new MutationObserver(() => { if (!this.editor!.Modal.isOpen()) { document.removeEventListener('keydown', handleKeyDown) modalObserver.disconnect() } }) const modalParent = this.editor.Modal.getContentEl()?.parentElement if (modalParent) { modalObserver.observe(modalParent, { attributes: true, attributeFilter: ['style', 'class'], }) } // Open the modal this.editor.Modal.open({ title, content: formElement, attributes: { class: 'ds-modal' }, }) } static styles = [ COMMON_STYLES, css` .ds-settings { padding: 16px 0; } .ds-empty-state { display: flex; align-items: center; justify-content: center; min-height: 200px; text-align: center; } .ds-empty-state__content { max-width: 300px; } .ds-empty-state__title { margin: 0 0 8px 0; font-size: 18px; color: inherit; } .ds-empty-state__description { margin: 0 0 24px 0; color: var(--gjs-light-color); line-height: 1.4; } .ds-data-sources { margin-bottom: 16px; } .ds-add-section { padding: 16px 0; border-top: 1px solid var(--ds-button-bg); text-align: center; } .ds-btn-prim--large { padding: 12px 24px; font-size: 14px; } `, ] protected render() { const visibleDataSources = this.dataSources.filter(ds => !ds.hidden) return html` <section class="ds-settings"> ${visibleDataSources.length === 0 ? html` <div class="ds-empty-state"> <div class="ds-empty-state__content"> <h3 class="ds-empty-state__title">No Data Sources</h3> <p class="ds-empty-state__description"> Connect to GraphQL APIs to display dynamic data in your website </p> <button type="button" class="ds-btn-prim ds-btn-prim--large" @click=${() => this.openDataSourceModal()} > Add Your First Data Source </button> </div> </div> ` : html` <div class="ds-data-sources"> ${repeat(visibleDataSources, (ds: IDataSource) => `${ds.id}-${this.updateCounter}`, (ds: IDataSource) => html` <ds-data-source-card .dataSource=${ds} data-id=${ds.id} @edit=${(e: CustomEvent) => this.openDataSourceModal(e.detail)} @test=${(e: CustomEvent) => this.testConnection(e.detail)} @delete=${(e: CustomEvent) => { this.dispatchEvent(new CustomEvent('delete', { detail: e.detail })) }} ></ds-data-source-card> `)} </div> <div class="ds-add-section"> <button type="button" class="ds-btn-prim" @click=${() => this.openDataSourceModal()} > Add Data Source </button> </div> `} </section> ` } testConnection(dataSource: IDataSource) { // Find the card component to update its loading state const cardElement = this.shadowRoot?.querySelector(`ds-data-source-card[data-id="${dataSource.id}"]`) as DataSourceCard if (cardElement) { cardElement.isTestingConnection = true } // Create a temporary form element to handle the connection test const tempForm = document.createElement('ds-settings__data-source') as SettingsDataSource tempForm.dataSource = dataSource // Override the original connectDataSource to handle card updates tempForm.connectDataSource = () => { if (cardElement) { cardElement.isTestingConnection = true } dataSource.connect().then(() => { if (cardElement) { cardElement.isTestingConnection = false } this.dispatchEvent(new CustomEvent('change', { detail: dataSource })) this.requestUpdate() }).catch((err: Error) => { console.error('Data source connection error', { err }) if (cardElement) { cardElement.isTestingConnection = false } this.requestUpdate() }) } tempForm.connectDataSource() } } if(!customElements.get('ds-settings')) { customElements.define('ds-settings', SettingsDataSources) } class SettingsDataSource extends LitElement { @property({ type: Object }) dataSource: IDataSource | null @property({ type: Boolean }) isEdit: boolean = false // State for the creation flow @state() errorMessage: string = '' @state() connected: boolean = false @state() isLoading: boolean = false @state() creationPhase: 'config' | 'types' = 'config' // config = initial form, types = type selection @state() availableTypes: LightweightType[] = [] // List of types (name + kind) from lightweight query @state() selectedTypes: Set<string> = new Set() // Currently selected types @state() typeSearchQuery: string = '' // Search/filter for type list @state() queryTypeName: string = 'Query' // The query type name from schema introspection private loadingIcon = html`<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="3"><animate attributeName="r" values="3;8;3" dur="1s" repeatCount="indefinite"/><animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite"/></circle></svg>` private connectedIcon = html`<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>` // private unknownIcon = html`<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="2"/></svg>` constructor() { super() this.dataSource = null } connectedCallback() { super.connectedCallback() } /** * Phase 1: Fetch lightweight type names from the GraphQL endpoint */ async fetchTypeNames() { if (!this.dataSource || !(this.dataSource instanceof GraphQL)) { this.errorMessage = 'Invalid data source' return } this.isLoading = true this.errorMessage = '' this.requestUpdate() try { const ds = this.dataSource as GraphQL const result = await ds.fetchTypeNames() // Store the queryType name from the schema this.queryTypeName = result.queryTypeName // Sort types by name this.availableTypes = result.types.sort((a, b) => a.name.localeCompare(b.name)) // For existing datasources with disabledTypes, compute selected = all - disabled // Otherwise, apply backend-specific default selection if (this.isEdit && ds.disabledTypes && ds.disabledTypes.length > 0) { const allTypeNames = this.availableTypes.map(t => t.name) const selectedNames = allTypeNames.filter(name => !ds.disabledTypes!.includes(name)) this.selectedTypes = new Set(selectedNames) } else { const backendType = ds.backendType || 'generic' const defaultEnabled = GraphQL.getDefaultEnabledTypes(backendType, this.availableTypes, this.queryTypeName) this.selectedTypes = new Set(defaultEnabled) } // Move to type selection phase this.creationPhase = 'types' this.isLoading = false this.requestUpdate() } catch (e) { console.error('[Settings] Failed to fetch type names:', e) this.errorMessage = (e as Error).message this.isLoading = false this.requestUpdate() } } /** * Check if a type is required (cannot be blacklisted) * The queryType and all SCALAR types are required */ isRequiredType(typeName: string): boolean { if (typeName === this.queryTypeName) return true const type = this.availableTypes.find(t => t.name === typeName) return type?.kind === 'SCALAR' } /** * Toggle a type's selected state */ toggleType(typeName: string) { // Query and SCALAR types cannot be unchecked - they're always required if (this.isRequiredType(typeName)) return if (this.selectedTypes.has(typeName)) { this.selectedTypes.delete(typeName) } else { this.selectedTypes.add(typeName) } this.selectedTypes = new Set(this.selectedTypes) // Trigger reactivity this.requestUpdate() } /** * Select all types */ selectAllTypes() { this.selectedTypes = new Set(this.availableTypes.map(t => t.name)) this.requestUpdate() } /** * Deselect all types (except queryType and SCALAR types which are always required) */ deselectAllTypes() { // Always keep queryType and SCALAR types selected const requiredTypes = this.availableTypes .filter(t => t.name === this.queryTypeName || t.kind === 'SCALAR') .map(t => t.name) this.selectedTypes = new Set(requiredTypes) this.requestUpdate() } /** * Reset types to default selection based on backend type */ resetToDefaults() { const isGraphQL = this.dataSource instanceof GraphQL if (!isGraphQL) return const backendType = (this.dataSource as GraphQL).backendType || 'generic' const defaultEnabled = GraphQL.getDefaultEnabledTypes(backendType, this.availableTypes, this.queryTypeName) this.selectedTypes = new Set(defaultEnabled) this.requestUpdate() } /** * Get filtered types based on search query * Selected types are always shown first */ getFilteredTypes(): LightweightType[] { let types = this.availableTypes if (this.typeSearchQuery) { const query = this.typeSearchQuery.toLowerCase() types = types.filter(t => t.name.toLowerCase().includes(query)) } // Sort: selected types first, then alphabetically within each group return types.sort((a, b) => { const aSelected = this.selectedTypes.has(a.name) const bSelected = this.selectedTypes.has(b.name) if (aSelected && !bSelected) return -1 if (!aSelected && bSelected) return 1 return a.name.localeCompare(b.name) }) } /** * Go back to config phase */ goBackToConfig() { this.creationPhase = 'config' this.availableTypes = [] this.selectedTypes = new Set() this.queryTypeName = 'Query' this.errorMessage = '' this.requestUpdate() } /** * Handle backend type change - updates selected types based on new backend defaults */ onBackendTypeChange(newBackendType: GraphQLBackendType) { if (!this.dataSource || !(this.dataSource instanceof GraphQL)) return const ds = this.dataSource as GraphQL ds.backendType = newBackendType // Apply new default selection based on backend type const defaultEnabled = GraphQL.getDefaultEnabledTypes(newBackendType, this.availableTypes, this.queryTypeName) this.selectedTypes = new Set(defaultEnabled) this.requestUpdate() } connectDataSource() { if(!this.dataSource) throw new Error('No data source provided') // Compute disabledTypes from selection (blacklist approach) if (this.dataSource instanceof GraphQL && this.availableTypes.length > 0) { const ds = this.dataSource as GraphQL const allTypeNames = this.availableTypes.map(t => t.name) const disabledTypes = allTypeNames.filter(name => !this.selectedTypes.has(name)) // Set disabled types for persistence (only blacklist, no whitelist) ds.disabledTypes = disabledTypes } this.isLoading = true this.errorMessage = '' this.requestUpdate() this.dataSource.connect().then(() => { this.dispatchEvent(new CustomEvent('change')) this.errorMessage = '' this.connected = true this.isLoading = false this.requestUpdate() }).catch((err: Error) => { console.error('Data source connection error', { err }) this.errorMessage = err.message this.connected = false this.isLoading = false this.requestUpdate() }) } static styles = [ css` form { display: flex; flex-direction: column; align-items: stretch; } .ds-field--large { flex: 1 1 auto; } .ds-property__wrapper { display: flex; flex-direction: column; width: 100%; flex-wrap: wrap; } .ds-property__wrapper--horiz { flex-direction: row; } .ds-property__wrapper--vert { flex-direction: column; } .ds-status-section { margin: 10px 0; } .ds-status-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 4px; font-size: 14px; } .ds-status-loading { background-color: var(--gjs-main-dark-color); color: var(--ds-highlight); } .ds-status-success { background-color: var(--gjs-main-dark-color); color: var(--ds-primary); } .ds-status-error { background-color: var(--gjs-main-dark-color); color: var(--ds-highlight); } .ds-actions { display: flex; gap: 10px; justify-content: space-between; align-items: center; margin-top: 15px; } .ds-actions-left { display: flex; gap: 10px; } .ds-actions-right { display: flex; gap: 10px; margin-left: auto; } .ds-no-resize { flex: 0 0 auto; } button:disabled { opacity: 0.6; cursor: not-allowed; } /* Backend selector styles */ .ds-backend-selector { margin-bottom: 15px; } .ds-backend-options { display: flex; gap: 10px; border: none; padding: 0; margin: 0; } .ds-backend-option { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 16px; border: 2px solid var(--gjs-main-dark-color); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; background: var(--gjs-main-dark-color); min-width: 80px; } .ds-backend-option:hover { border-color: var(--gjs-light-color); } .ds-backend-option--selected { border-color: var(--ds-primary); background: rgba(var(--ds-primary-rgb, 76, 175, 80), 0.1); } .ds-backend-option input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; } .ds-backend-option svg { width: 40px; height: 40px; } .ds-backend-option span { font-size: 11px; color: var(--gjs-light-color); text-align: center; } .ds-backend-option--beta { position: relative; } .ds-backend-option--beta::after { content: 'beta'; position: absolute; top: 4px; right: 4px; padding: 1px 4px; padding-top: 3px; font-size: smaller; font-weight: bold; text-transform: uppercase; background: var(--ds-primary); color: var(--gjs-secondary-color); border-radius: 3px; line-height: 1.2; } /* Inline backend selector (compact for config phase) */ .ds-backend-selector--inline { display: flex; gap: 8px; margin-top: 8px; } .ds-backend-option--compact { flex-direction: row; padding: 8px 14px; min-width: auto; gap: 8px; } .ds-backend-option--compact svg { width: 20px; height: 20px; } .ds-backend-option--compact span { font-size: 13px; } /* Advanced options accordion */ .ds-advanced-details { margin: 15px 0; border: 1px solid var(--gjs-main-dark-color); border-radius: 6px; background: var(--gjs-main-dark-color); } .ds-advanced-summary { padding: 12px 15px; cursor: pointer; font-size: 13px; color: var(--gjs-light-color); user-select: none; display: flex; align-items: center; gap: 8px; } .ds-advanced-summary:hover { color: inherit; } .ds-advanced-summary::before { content: '▶'; font-size: 10px; transition: transform 0.2s; } .ds-advanced-details[open] .ds-advanced-summary::before { transform: rotate(90deg); } .ds-advanced-content { padding: 0 15px 15px; border-top: 1px solid var(--gjs-main-light-color); } .ds-hint { display: block; margin-top: 5px; font-size: 11px; color: var(--gjs-light-color); } .ds-help-text { margin: 0 0 10px; font-size: 11px; color: var(--gjs-light-color); font-style: italic; } .ds-types-count-inline { font-size: 12px; color: var(--gjs-light-color); margin-left: auto; } /* Type selection styles */ .ds-types-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; flex-wrap: wrap; gap: 10px; } .ds-types-search { flex: 1; min-width: 150px; } .ds-types-actions { display: flex; gap: 5px; } .ds-types-actions button { padding: 4px 8px; font-size: 12px; } .ds-types-list { max-height: 300px; overflow-y: auto; border: 1px solid var(--gjs-main-dark-color); border-radius: 4px; padding: 8px; background-color: var(--gjs-main-dark-color); } .ds-type-item { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 3px; cursor: pointer; transition: background-color 0.15s; } .ds-type-item:hover { background-color: var(--gjs-main-light-color); } .ds-type-item input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; } .ds-type-item label { cursor: pointer; flex: 1; font-family: monospace; font-size: 13px; } .ds-type-kind { font-size: 10px; padding: 2px 5px; border-radius: 3px; background-color: var(--gjs-main-light-color); color: var(--gjs-light-color); text-transform: lowercase; } .ds-type-kind--scalar { background-color: rgba(100, 200, 100, 0.2); color: #4caf50; } .ds-type-kind--object { background-color: rgba(100, 150, 255, 0.2); color: #2196f3; } .ds-type-kind--interface { background-color: rgba(200, 150, 255, 0.2); color: #9c27b0; } .ds-type-kind--enum { background-color: rgba(255, 200, 100, 0.2); color: #ff9800; } .ds-type-kind--union { background-color: rgba(255, 150, 150, 0.2); color: #f44336; } .ds-type-kind--input_object { background-color: rgba(150, 200, 200, 0.2); color: #00bcd4; } .ds-types-count { font-size: 12px; color: var(--gjs-light-color); margin-top: 8px; } `, COMMON_STYLES, ] /** * Render the configuration form (Phase 1) */ renderConfigPhase(): TemplateResult { if (!this.dataSource) throw new Error('No data source provided') const dsHeaders: Ref<SettingsHeaders> = createRef() return html` <h3 class="ds-property__title"> ${this.dataSource.label || 'Unnamed'} <small>${this.dataSource.readonly !== false ? ' (Read-only)' : ''}</small> </h3> <div class="ds-property__wrapper ds-property__wrapper--horiz"> <label class="ds-field"> <span>Label</span> <input type="text" name="label" value=${this.dataSource.label} @input=${(e: Event) => { if (this.dataSource) this.dataSource.label = (e.target as HTMLInputElement).value this.requestUpdate() }} ?readonly=${this.dataSource.readonly !== false} /> </label> <label class="ds-field ds-field--large"> <span>URL</span> <input type="url" name="url" value=${this.dataSource.url} @input=${(e: Event) => { if (this.dataSource) { this.dataSource.url = (e.target as HTMLInputElement).value this.requestUpdate() } }} ?readonly=${this.dataSource.readonly !== false} /> </label> <!-- ID field hidden - not needed by users, auto-generated --> <label class="ds-field"> <span>Method</span> <select name="method" @change=${(e: Event) => { if (this.dataSource) this.dataSource.method = (e.target as HTMLInputElement).value as 'GET' | 'POST' }} ?readonly=${this.dataSource.readonly !== false} ?disabled=${this.dataSource.readonly !== false} > <option value="POST" ?selected=${this.dataSource.method === 'POST'}>POST</option> <option value="GET" ?selected=${this.dataSource.method === 'GET'}>GET</option> </select> </label> </div> <details class="ds-advanced-details"> <summary class="ds-advanced-summary">HTTP Headers</summary> <div class="ds-advanced-content"> <ds-settings__headers ${ref(dsHeaders)} .headers=${this.dataSource.headers} @change=${() => { if (this.dataSource) this.dataSource.headers = dsHeaders.value?.headers || {} dsHeaders.value?.requestUpdate() }} ?readonly=${this.dataSource.readonly !== false} ></ds-settings__headers> </div> </details> <div class="ds-field"> <div class="ds-status-section"> ${this.isLoading ? html`<div class="ds-status-item ds-status-loading">${this.loadingIcon} Fetching types...</div>` : this.errorMessage ? html`<div class="ds-status-item ds-status-error">Error: ${this.errorMessage}</div>` : '' } </div> </div> <div class="ds-field ds-actions"> <div class="ds-actions-right"> <button type="button" class="ds-btn-prim" @click=${() => this.dispatchEvent(new CustomEvent('cancel'))} >Cancel</button> ${this.isEdit ? html` <button type="button" class="ds-btn-prim" ?disabled=${this.isLoading || !this.dataSource.url} @click=${() => this.fetchTypeNames()} >${this.isLoading ? 'Loading...' : 'Options'}</button> <button type="submit" class="ds-btn-prim" ?disabled=${this.isLoading || !this.dataSource.url} >${this.isLoading ? 'Applying...' : 'Apply'}</button> ` : html` <button type="submit" class="ds-btn-prim" ?disabled=${this.isLoading || !this.dataSource.url} >${this.isLoading ? 'Fetching...' : 'Next'}</button> `} </div> </div> </div> ` } /** * Render the type selection phase (Phase 2) */ renderTypesPhase(): TemplateResult { const filteredTypes = this.getFilteredTypes() const selectedCount = this.selectedTypes.size const totalCount = this.availableTypes.length const isGraphQL = this.dataSource instanceof GraphQL const backendType = isGraphQL ? (this.dataSource as GraphQL).backendType : 'generic' return html` <h3 class="ds-property__title">Data Source Options</h3> <p class="ds-help-text">Select your backend type. Any GraphQL API is a supported, choose "GraphQL" for a generic connector.</p> <div class="ds-backend-selector"> <fieldset class="ds-backend-options"> <label class="ds-backend-option ${backendType === 'generic' ? 'ds-backend-option--selected' : ''}"> <input type="radio" name="backendType" value="generic" ?checked=${backendType === 'generic'} @change=${() => this.onBackendTypeChange('generic')} /> <svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg"> <path fill="#E535AB" d="M57.468 302.66l-14.376-8.3 160.15-277.38 14.376 8.3z"/> <path fill="#E535AB" d="M39.8 272.2h320.3v16.6H39.8z"/> <path fill="#E535AB" d="M206.348 374.026l-160.21-92.5 8.3-14.376 160.21 92.5zM345.522 132.947l-160.21-92.5 8.3-14.376 160.21 92.5z"/> <path fill="#E535AB" d="M54.482 132.883l-8.3-14.375 160.21-92.5 8.3 14.376z"/> <path fill="#E535AB" d="M342.568 302.663l-160.15-277.38 14.376-8.3 160.15 277.38zM52.5 107.5h16.6v185H52.5zM330.9 107.5h16.6v185h-16.6z"/> <path fill="#E535AB" d="M203.522 367l-7.25-12.558 139.34-80.45 7.25 12.557z"/> <path fill="#E535AB" d="M369.5 297.9c-9.6 16.7-31 22.4-47.7 12.8-16.7-9.6-22.4-31-12.8-47.7 9.6-16.7 31-22.4 47.7-12.8 16.8 9.7 22.5 31 12.8 47.7M90.9 137c-9.6 16.7-31 22.4-47.7 12.8-16.7-9.6-22.4-31-12.8-47.7 9.6-16.7 31-22.4 47.7-12.8 16.7 9.7 22.4 31 12.8 47.7M30.5 297.9c-9.6-16.7-3.9-38 12.8-47.7 16.7-9.6 38-3.9 47.7 12.8 9.6 16.7 3.9 38-12.8 47.7-16.8 9.6-38.1 3.9-47.7-12.8M309.1 137c-9.6-16.7-3.9-38 12.8-47.7 16.7-9.6 38-3.9 47.7 12.8 9.6 16.7 3.9 38-12.8 47.7-16.7 9.6-38.1 3.9-47.7-12.8M200 395.8c-19.3 0-34.9-15.6-34.9-34.9 0-19.3 15.6-34.9 34.9-34.9 19.3 0 34.9 15.6 34.9 34.9 0 19.2-15.6 34.9-34.9 34.9M200 74c-19.3 0-34.9-15.6-34.9-34.9 0-19.3 15.6-34.9 34.9-34.9 19.3 0 34.9 15.6 34.9 34.9 0 19.3-15.6 34.9-34.9 34.9"/> </svg> <span>GraphQL</span> </label> <label class="ds-backend-option ds-backend-option--beta ${backendType === 'wordpress' ? 'ds-backend-option--selected' : ''}"> <input type="radio" name="backendType" value="wordpress" ?checked=${backendType === 'wordpress'} @change=${() => this.onBackendTypeChange('wordpress')} /> <svg viewBox="0 0 122.52 122.523" xmlns="http://www.w3.org/2000/svg"> <g fill="#21759b"> <path d="m8.708 61.26c0 20.802 12.089 38.779 29.619 47.298l-25.069-68.686c-2.916 6.536-4.55 13.769-4.55 21.388z"/> <path d="m96.74 58.608c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.501-34.493-8.188-22.434c-2.83-.166-5.511-.501-5.511-.501-2.832-.166-2.5-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.853.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989z"/> <path d="m62.184 65.857-15.768 45.819c4.708 1.384 9.687 2.141 14.846 2.141 6.12 0 11.989-1.058 17.452-2.979-.14-.225-.269-.464-.374-.724z"/> <path d="m107.376 36.046c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215z"/> <path d="m61.262 0c-33.779 0-61.262 27.481-61.262 61.26 0 33.783 27.483 61.263 61.262 61.263 33.778 0 61.265-27.48 61.265-61.263-.001-33.779-27.487-61.26-61.265-61.26zm0 119.715c-32.23 0-58.453-26.223-58.453-58.455 0-32.23 26.222-58.451 58.453-58.451 32.229 0 58.45 26.221 58.45 58.451 0 32.232-26.221 58.455-58.45 58.455z"/> </g> </svg> <span>WordPress</span> </label> <label class="ds-backend-option ds-backend-option--beta ${backendType === 'gitlab' ? 'ds-backend-option--selected' : ''}"> <input type="radio" name="backendType" value="gitlab" ?checked=${backendType === 'gitlab'} @change=${() => this.onBackendTypeChange('gitlab')} /> <svg viewBox="0 0 380 380" xmlns="http://www.w3.org/2000/svg"> <path fill="#e24329" d="M190 362.6 256.3 158.8H123.7z"/> <path fill="#fc6d26" d="M190 362.6 123.7 158.8H15.3z"/> <path fill="#fca326" d="m15.3 158.8-13.5 41.6c-1.2 3.8.1 7.9 3.3 10.3L190 362.6z"/> <path fill="#e24329" d="M15.3 158.8h108.4L79.8 24.1c-1.4-4.4-7.6-4.4-9 0z"/> <path fill="#fc6d26" d="m190 362.6 66.3-203.8h108.4z"/> <path fill="#fca326" d="m364.7 158.8 13.5 41.6c1.2 3.8-.1 7.9-3.3 10.3L190 362.6z"/> <path fill="#e24329" d="M364.7 158.8H256.3l43.9-134.7c1.4-4.4 7.6-4.4 9 0z"/> </svg> <span>GitLab</span> </label> <label class="ds-backend-option ds-backend-option--beta ${backendType === 'strapi' ? 'ds-backend-option--selected' : ''}"> <input type="radio" name="backendType" value="strapi" ?checked=${backendType === 'strapi'} @change=${() => this.onBackendTypeChange('strapi')} /> <svg viewBox=".24262095 .26549587 243.32256626 243.58072911" xmlns="http://www.w3.org/2000/svg"> <g fill="#8e75ff" fill-rule="evenodd"> <path d="m161.893 165.833v-78.73a5.077 5.077 0 0 0 -5.077-5.076h-78.638v-81.267h159.815a5.077 5.077 0 0 1 5.078 5.077v159.996z"/> <path d="m78.178.76v81.267h-75.054a2.539 2.539 0 0 1 -1.796-4.333zm83.715 240.206v-75.133h81.178l-76.844 76.927a2.539 2.539 0 0 1 -4.334-1.794zm-83.715-158.939h81.176a2.539 2.539 0 0 1 2.539 2.538v81.268h-78.638a5.077 5.077 0 0 1 -5.077-5.077z" opacity=".405"/> </g> </svg> <span>Strapi</span> </label> <label class="ds-backend-option ds-backend-option--beta ${backendType === 'supabase' ? 'ds-backend-option--selected' : ''}"> <input type="radio" name="backendType" value="supabase" ?checked=${backendType === 'supabase'} @change=${() => this.onBackendTypeChange('supabase')} /> <svg viewBox="0 0 109 113" xmlns="http://www.w3.org/2000/svg"> <path fill="url(#supabase-a)" d="M63.7 110.3c-2.5 3.1-7.6 1.4-7.7-2.6l-1.5-66.2h50.2c9.1 0 14.1 10.5 8.4 17.6L63.7 110.3Z"/> <path fill="url(#supabase-b)" fill-opacity=".2" d="M63.7 110.3c-2.5 3.1-7.6 1.4-7.7-2.6l-1.5-66.2h50.2c9.1 0 14.1 10.5 8.4 17.6L63.7 110.3Z"/> <path fill="#3ECF8E" d="M45.3 2.7c2.5-3.1 7.6-1.4 7.7 2.6l.7 66.2H4.4c-9.1 0-14-10.5-8.3-17.6L45.3 2.7Z"/> <defs> <linearGradient id="supabase-a" x1="53.97" x2="94.12" y1="54.97" y2="71.95" gradientUnits="userSpaceOnUse"> <stop stop-color="#249361"/><stop offset="1" stop-color="#3ECF8E"/> </linearGradient> <linearGradient id="supabase-b" x1="36.16" x2="54.48" y1="30.58" y2="65.72" gradientUnits="userSpaceOnUse"> <stop/><stop offset="1" stop-opacity="0"/> </linearGradient> </defs> </svg> <span>Supabase</span> </label> </fieldset> </div> <!-- Advanced: Manual type selection --> <details class="ds-advanced-details"> <summary class="ds-advanced-summary"> Show / hide fields in the expressions <span class="ds-types-count-inline">(${selectedCount}/${totalCount})</span> </summary> <div class="ds-advanced-content"> <p class="ds-help-text"><br>Uncheck types you don't want to see in your expressions.</p> <div class="ds-types-header"> <input type="text" class="ds-types-search" placeholder="Search types..." .value=${this.typeSearchQuery} @input=${(e: Event) => { this.typeSearchQuery = (e.target as HTMLInputElement).value this.requestUpdate() }} /> <div class="ds-types-actions"> <button type="button" class="ds-btn-prim" @click=${() => this.selectAllTypes()}>Select All</button> <button type="button" class="ds-btn-prim" @click=${() => this.deselectAllTypes()}>Deselect All</button> <button type="button" class="ds-btn-prim" @click=${() => this.resetToDefaults()}>Reset to Defaults</button> </div> </div> <div class="ds-types-list"> ${filteredTypes.map(type => { const isRequired = this.isRequiredType(type.name) return html` <div class="ds-type-item" @click=${() => this.toggleType(type.name)}> <input type="checkbox" .checked=${this.selectedTypes.has(type.name)} ?disabled=${isRequired} @click=${(e: Event) => e.stopPropagation()} @change=${() => this.toggleType(type.name)} /> <label>${type.name}</label> <span class="ds-type-kind ds-type-kind--${type.kind.toLowerCase()}">${type.kind}</span> </div> `})} ${filteredTypes.length === 0 ? html` <div style="padding: 20px; text-align: center; color: var(--gjs-light-color);"> No types match "${this.typeSearchQuery}" </div> ` : ''} </div> <div class="ds-types-count"> ${selectedCount} of ${totalCount} types selected ${this.typeSearchQuery ? ` (showing ${filteredTypes.length} matching "${this.typeSearchQuery}")` : ''} </div> </div> </details> <div class="ds-field"> <div class="ds-status-section"> ${this.isLoading ? html`<div class="ds-status-item ds-status-loading">${this.loadingIcon} Connecting...</div>` : this.errorMessage ? html`<div class="ds-status-item ds-status-error">Error: ${this.errorMessage}</div>` : this.connected ? html`<div class="ds-status-item ds-status-success">${this.connectedIcon} Connection successful</div>` : '' } </div> </div> <div class="ds-field ds-actions"> <div class="ds-actions-left"> <button type="button" class="ds-btn-prim" @click=${() => this.goBackToConfig()} ?disabled=${this.isLoading} >Back</button> </div> <div class="ds-actions-right"> <button type="button" class="ds-btn-prim" @click=${() => this.dispatchEvent(new CustomEvent('cancel'))} >Cancel</button> <button type="submit" class="ds-btn-prim" ?disabled=${this.isLoading || this.selectedTypes.size === 0} >${this.isLoading ? 'Connecting...' : 'Apply'}</button> </div> </div> ` } protected render() { if (!this.dataSource) throw new Error('No data source provided') return html` <form ?readonly=${this.dataSource.readonly !== false} @submit=${(e: Event) => { e.preventDefault() e.stopImmediatePropagation() // Handle form submission based on current phase if (this.creationPhase === 'config') { if (this.isEdit) { this.connectDataSource() } else { this.fetchTypeNames() } } else { this.connectDataSource() } }} > ${this.creationPhase === 'config' ? this.renderConfigPhase() : this.renderTypesPhase()} </form> ` } } if(!customElements.get('ds-settings__data-source')) { customElements.define('ds-settings__data-source', SettingsDataSource) } /** * Compact card component for displaying data source summary */ class DataSourceCard extends LitElement { @property({ type: Object }) dataSource: IDataSource | null @property({ type: Boolean }) isTestingConnection: boolean = false private loadingIcon = html`<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="3"><animate attributeName="r" values="3;8;3" dur="1s" repeatCount="indefinite"/><animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite"/></circle></svg>` // private connectedIcon = html`<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>` // private errorIcon = html`<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>` constructor() { super() this.dataSource = null this.isTestingConnection = false } static styles = [ css` .ds-card { background-color: var(--gjs-main-dark-color); border: 1px solid var(--ds-button-bg); border-radius: 6px; padding: 16px; margin: 8px 0; display: flex; justify-content: space-between; align-items: center; transition: border-color 0.2s ease, background-color 0.2s ease; cursor: pointer; } .ds-card:hover { border-color: var(--ds-highlight); background-color: rgba(255, 255, 255, 0.02); } .ds-card__info { flex: 1; min-width: 0; } .ds-card__title { font-weight: 500; margin: 0 0 4px 0; color: inherit; display: flex; align-items: flex-end; } .ds-card__url { font-size: 12px; color: var(--gjs-light-color); margin: 0 0 4px 0; word-break: break-all; } .ds-card__logo { width: 18px; height: 18px; flex-shrink: 0; margin-right: 8px; vertical-align: middle; } .ds-card__backend { display: inline-block; font-size: 11px; font-weight: normal; color: var(--gjs-light-color); background-color: var(--gjs-main-light-color); padding: 2px 6px; border-radius: 3px; margin-left: 8px; vertical-align: middle; } .ds-card__status { display: flex; align-items: center; gap: 6px; font-size: 12px; } .ds-card__status--connected { color: var(--ds-primary); } .ds-card__status--loading { color: var(--ds-highlight); } .ds-card__status--error { color: var(--ds-highlight); } .ds-card__actions { display: flex; gap: 4px; flex-shrink: 0; opacity: 0.6; transition: opacity 0.2s ease; } .ds-card:hover .ds-card__actions { opacity: 1; } .ds-card__btn { padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer; font-size: 11px; background-color: transparent; color: var(--gjs-light-color); transition: background-color 0.2s ease, color 0.2s ease; } .ds-card__btn:hover { background-color: var(--gjs-main-light-color); color: inherit; } .ds-card__btn--danger { color: var(--ds-highlight); } .ds-card__btn--danger:hover { background-color: rgba(255, 0, 0, 0.1); } .ds-card__btn:disabled { opacity: 0.6; cursor: not-allowed; } `, COMMON_STYLES, ] private graphqlLogo = html`<svg class="ds-card__logo" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg"><path fill="#E535AB" d="M57.468 302.66l-14.376-8.3 160.15-277.38 14.376 8.3z"/><path fill="#E535AB" d="M39.8 272.2h320.3v16.6H39.8z"/><path fill="#E535AB" d="M206.348 374.026l-160.21-92.5 8.3-14.376 160.21 92.5zM345.522 132.947l-160.21-92.5 8.3-14.376 160.21 92.5z"/><path fill="#E535AB" d="M54.482 132.883l-8.3-14.375 160.21-92.5 8.3 14.376z"/><path fill="#E535AB" d="M342.568 302.663l-160.15-277.38 14.376-8.3 160.15 277.38zM52.5 107.5h16.6v185H52.5zM330.9 107.5h16.6v185h-16.6z"/><path fill="#E535AB" d="M203.522 367l-7.25-12.558 139.34-80.45 7.25 12.557z"/><path fill="#E535AB" d="M369.5 297.9c-9.6 16.7-31 22.4-47.7 12.8-16.7-9.6-22.4-31-12.8-47.7 9.6-16.7 31-22.4 47.7-12.8 16.8 9.7 22.5 31 12.8 47.7M90.9 137c-9.6 16.7-31 22.4-47.7 12.8-16.7-9.6-22.4-31-12.8-47.7 9.6-16.7 31-22.4 47.7-12.8 16.7 9.7 22.4 31 12.8 47.7M30.5 297.9c-9.6-16.7-3.9-38 12.8-47.7 16.7-9.6 38-3.9 47.7 12.8 9.6 16.7 3.9 38-12.8 47.7-16.8 9.6-38.1 3.9-47.7-12.8M309.1 137c-9.6-16.7-3.9-38 12.8-47.7 16.7-9.6 38-3.9 47.7 12.8 9.6 16.7 3.9 38-12.8 47.7-16.7 9.6-38.1 3.9-47.7-12.8M200 395.8c-19.3 0-34.9-15.6-34.9-34.9 0-19.3 15.6-34.9 34.9-34.9 19.3 0 34.9 15.6 34.9 34.9 0 19.2-15.6 34.9-34.9 34.9M200 74c-19.3 0-34.9-15.6-34.9-34.9 0-19.3 15.6-34.9 34.9-34.9 19.3 0 34.9 15.6 34.9 34.9 0 19.3-15.6 34.9-34.9 34.9"/></svg>` private wordpressLogo = html`<svg class="ds-card__logo" viewBox="0 0 122.52 122.523" xmlns="http://www.w3.org/2000/svg"><g fill="#21759b"><path d="m8.708 61.26c0 20.802 12.089 38.779 29.619 47.298l-25.069-68.686c-2.916 6.536-4.55 13.769-4.55 21.388z"/><path d="m96.74 58.608c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.501-34.493-8.188-22.434c-2.83-.166-5.511-.501-5.511-.501-2.832-.166-2.5-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.853.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989z"/><path d="m62.184 65.857-15.768 45.819c4.708 1.384 9.687 2.141 14.846 2.141 6.12 0 11.989-1.058 17.452-2.979-.14-.225-.269-.464-.374-.724z"/><path d="m107.376 36.046c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215z"/><path d="m61.262 0c-33.779 0-61.262 27.481-61.262 61.26 0 33.783 27.483 61.263 61.262 61.263 33.778 0 61.265-27.48 61.265-61.263-.001-33.779-27.487-61.26-61.265-61.26zm0 119.715c-32.23 0-58.453-26.223-58.453-58.455 0-32.23 26.222-58.451 58.453-58.451 32.229 0 58.45 26.221 58.45 58.451 0 32.232-26.221 58.455-58.45 58.455z"/></g></svg>` private gitlabLogo = html`<svg class="ds-card__logo" viewBox="0 0 380 380" xmlns="http://www.w3.org/2000/svg"><path fill="#e24329" d="M190 362.6 256.3 158.8H123.7z"/><path fill="#fc6d26" d="M190 362.6 123.7 158.8H15.3z"/><path fill="#fca326" d="m15.3 158.8-13.5 41.6c-1.2 3.8.1 7.9 3.3 10.3L190 362.6z"/><path fill="#e24329" d="M15.3 158.8h108.4L79.8 24.1c-1.4-4.4-7.6-4.4-9 0z"/><path fill="#fc6d26" d="m190 362.6 66.3-203.8h108.4z"/><path fill="#fca326" d="m364.7 158.8 13.5 41.6c1.2 3.8-.1 7.9-3.3 10.3L190 362.6z"/><path fill="#e24329" d="M364.7 158.8H256.3l43.9-134.7c1.4-4.4 7.6-4.4 9 0z"/></svg>` private strapiLogo = html`<svg class="ds-card__logo" viewBox=".24262095 .26549587 243.32256626 243.58072911" xmlns="http://www.w3.org/2000/svg"><g fill="#8e75ff" fill-rule="evenodd"><path d="m161.893 165.833v-78.73a5.077 5.077 0 0 0 -5.077-5.076h-78.638v-81.267h159.815a5.077 5.077 0 0 1 5.078 5.077v159.996z"/><path d="m78.178.76v81.267h-75.054a2.539 2.539 0 0 1 -1.796-4.333zm83.715 240.206v-75.133h81.178l-76.844 76.927a2.539 2.539 0 0 1 -4.334-1.794zm-83.715-158.939h81.176a2.539 2.539 0 0 1 2.539 2.538v81.268h-78.638a5.077 5.077 0 0 1 -5.077-5.077z" opacity=".405"/></g></svg>` private supabaseLogo = html`<svg class="ds-card__logo" viewBox="0 0 109 113" xmlns="http://www.w3.org/2000/svg"><path fill="#3ECF8E" d="M63.7 110.3c-2.5 3.1-7.6 1.4-7.7-2.6l-1.5-66.2h50.2c9.1 0 14.1 10.5 8.4 17.6L63.7 110.3Z"/><path fill="#3ECF8E" d="M45.3 2.7c2.5-3.1 7.6-1.4 7.7 2.6l.7 66.2H4.4c-9.1 0-14-10.5-8.3-17.6L45.3 2.7Z"/></s