Skip to content

Commit

Permalink
Merge pull request #155 from radio4000/feat/queryparams
Browse files Browse the repository at this point in the history
Feat/queryparams
  • Loading branch information
oskarrough authored Nov 10, 2023
2 parents 11c416e + 74a071d commit f042472
Show file tree
Hide file tree
Showing 22 changed files with 450 additions and 529 deletions.
10 changes: 8 additions & 2 deletions examples/r4-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,20 @@
<script>
const $app = document.createElement('r4-app')
const params = new URLSearchParams(window.location.search)
const channel = params.get('channel')
const singleChannel = params.has('single-channel')

//$app.setAttribute('cdn', true)
//$app.setAttribute('cdn', 'https://jsdelivr.net/npm/@radio4000/components')

$app.setAttribute('href', window.location.origin + '/examples/r4-app/')

// If ?channel, start the app in single-channel mode.
const channel = params.get('channel')
channel && $app.setAttribute('channel', channel)

// If ?single-channel, start in single-channel mode.
const singleChannel = params.has('single-channel')
singleChannel && $app.setAttribute('single-channel', true)

/* !import.meta.env.PROD && $app.setAttribute('themes-url', 'http://localhost:4001') */
document.querySelector('body').append($app)
</script>
Expand Down
2 changes: 1 addition & 1 deletion examples/r4-supabase-filter-search/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
$search.addEventListener('input', ({detail}) => {
console.log('supabase-filter-search event', detail)
if (detail) {
window.history.replaceState(null, null, `?${paramName}=${JSON.stringify(detail)}`)
window.history.replaceState(null, null, `?${paramName}=${JSON.stringify(detail.filter)}`)
} else {
window.history.replaceState(null, null, './')
}
Expand Down
11 changes: 0 additions & 11 deletions examples/r4-supabase-query/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,9 @@
import R4SupabaseQuery from '../../src/components/r4-supabase-query.js'
import urlUtils from '../../src/libs/url-utils.js'

const {
propertiesToSearch,
propertiesFromSearch,
} = urlUtils

const $examples = document.querySelectorAll('r4-supabase-query')

const onQuery = function (event) {
/*
console.log('example query', event.detail)
const urlSearch = propertiesToSearch(elementProperties, detail)
const urlSearchString = `?${urlSearch.toString()}`
window.history.replaceState(null, null, urlSearchString)
*/
document.querySelector(`textarea[name="${event.target.id}"]`).innerText = JSON.stringify(event.detail)
}

Expand Down
3 changes: 3 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import R4Actions from './r4-actions.js'
import R4App from './r4-app.js'
import R4BaseQuery from './r4-base-query.js'
import R4AppMenu from './r4-app-menu.js'
import R4Avatar from './r4-avatar.js'
import R4AvatarUpdate from './r4-avatar-update.js'
Expand Down Expand Up @@ -55,6 +56,7 @@ customElements.define('r4-avatar', R4Avatar)
customElements.define('r4-avatar-update', R4AvatarUpdate)
customElements.define('r4-avatar-upload', R4AvatarUpload)
customElements.define('r4-auth-status', R4AuthStatus)
customElements.define('r4-base-query', R4BaseQuery)
customElements.define('r4-button-play', R4ButtonPlay)
customElements.define('r4-button-follow', R4ButtonFollow)
customElements.define('r4-channel', R4Channel)
Expand Down Expand Up @@ -102,6 +104,7 @@ export default {
R4AvatarUpdate,
R4AvatarUpload,
R4AuthStatus,
R4BaseQuery,
R4ButtonPlay,
R4ButtonFollow,
R4Channel,
Expand Down
165 changes: 165 additions & 0 deletions src/components/r4-base-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import {LitElement, html} from 'lit'
import debounce from 'lodash.debounce'
import urlUtils from '../libs/url-utils.js'
import {browse} from '../libs/browse.js'

// This is not in use anywhere.
// It is a sketch for later.

/**
* @typedef {object} R4Query
* @prop {string} [table] - table name
* @prop {string} [select] - sql query to select columns
* @prop {string} [search] - search query
* @prop {R4Filter[]} [filters] - array of filters
* @prop {string} [orderBy] - column name to order by
* @prop {string} [order] - 'asc' or 'desc'
* @prop {{ascending: boolean}} [orderConfig] - used internally to keep track of the order and respect foreign tables
* @prop {number} [page] - page number
* @prop {number} [limit] - items per page
*/

/**
* @typedef {object} R4Filter
* @prop {string} column - column name
* @prop {string} operator - operator
* @prop {string} value - value
*/

/**
* Adds all the neccessary things to query the database with search, filters, ordering and pagination.
*/
export default class R4BaseQuery extends LitElement {
static properties = {
// pass these down
initialQuery: {type: Object},
defaultFilters: {type: Array},
// inside we have
query: {type: Object, state: true},
data: {type: Array, state: true},
count: {type: Number, state: true},
}

constructor() {
super()

/** The amount of rows returned by fetchData */
this.count = 0

/** The latest data from fetchData */
this.data = []

/** @type {R4Query} */
this.initialQuery = {}
this.query = {}

/** @type {R4Filter[]} */
this.defaultFilters = []

/** A debounced version of fetchData() */
this.debouncedFetchData = debounce(() => this.fetchData(), 400, {leading: true, trailing: true})
}

connectedCallback() {
// As soon as the DOM is ready, read the URL query params
this.query = {...this.initialQuery, ...urlUtils.getQueryFromUrl(new URLSearchParams(location.search))}
console.log('<base-query> connected', this.query)
super.connectedCallback()
}

/**
* Essentially this.query + this.defaultFilters
* This exists in order to apply query changes that won't appear in the UI.
* @returns {R4Query}
*/
get browseQuery() {
const q = {...this.query}
// Apply default filters if there are some.
if (q.filters?.length) {
q.filters = [...q.filters]
} else {
q.filters = this.defaultFilters
}
// Apply search filter if there is a search query.
if (this.query.search) {
if (!q.filters) q.filters = []
q.filters = [...q.filters, urlUtils.createSearchFilter(this.query.search)]
}
return q
}

async fetchData() {
const res = await browse(this.browseQuery)
if (res.error) console.log('error fetching data', res)

// reset pagination while searching?
if (res.error?.code === 'PGRST103') {
this.setQuery({page: 1, limit: 10})
}

this.count = res.count
this.data = res.data
this.dispatchEvent(new CustomEvent('data', {detail: res}))
}

onQuery(event) {
event.preventDefault()
this.setQuery(event.detail)
}

onSearch(event) {
event.preventDefault()
const {search} = event.detail
this.setQuery({search})
}

clearFilters() {
this.setQuery({...this.query, filters: []})
}

// Just a shortcut when no extra logic is needed. Also updates URL params and reloads data.
setQuery(query) {
this.query = {...this.query, ...query}
urlUtils.setSearchParams(this.query)
this.debouncedFetchData()
}

render() {
return html`${this.renderControls()}`
}

renderControls() {
const filtersLen = this.query?.filters?.length
return html`
<menu>
<li>
<r4-supabase-filter-search
search=${this.query?.search}
placeholder=${this.count + ' rows'}
@input=${this.onSearch}
></r4-supabase-filter-search>
</li>
</menu>
<details open="true">
<summary>
Filters ${filtersLen ? html`<button @click=${this.clearFilters}>Clear ${filtersLen}</button>` : null}
</summary>
<r4-supabase-query
table=${this.query?.table}
.filters=${this.query?.filters}
order-by=${this.query?.orderBy}
order=${this.query?.order}
search=${this.query?.search}
page=${this.query?.page}
limit=${this.query?.limit}
count=${this.count}
@query=${this.onQuery}
></r4-supabase-query>
</details>
`
}

createRenderRoot() {
return this
}
}
5 changes: 4 additions & 1 deletion src/components/r4-page.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {html, LitElement} from 'lit'
import {LitElement, html} from 'lit'

/**
* A DOM structure/pattern for pages to re-use for consistency
*/
export default class R4Page extends LitElement {
render() {
return html`
Expand Down
3 changes: 2 additions & 1 deletion src/components/r4-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default class R4Router extends LitElement {
this.params = ctx.params
this.searchParams = ctx.searchParams
this.componentName = `r4-page-${page}`
console.log('<router>', ctx.querystring)
// Schedules a new render.
this.requestUpdate()
}
Expand All @@ -80,7 +81,7 @@ export default class R4Router extends LitElement {
if (!this.componentName) return
const tag = literal`${unsafeStatic(this.componentName)}`
// eslint-disable-next-line
const $pageDom = html`<${tag} .store=${this.store} .config=${this.config} .searchParams=${this.searchParams} .params=${this.params}></${tag}>`
const $pageDom = html`<${tag} .store=${this.store} .config=${this.config} .params=${this.params} .searchParams=${this.searchParams}></${tag}>`
return $pageDom
}

Expand Down
29 changes: 8 additions & 21 deletions src/components/r4-supabase-filter-search.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
import {LitElement, html} from 'lit'
import {createSearchFilter} from '../libs/url-utils.js'

// Renders a search input, and fires @input event with a Supabase SDK search filter.
export default class R4SupabaseFilterSearch extends LitElement {
static properties = {
search: {type: String, state: true},
search: {type: String},
placeholder: {type: String},
}

// should be externilized in the sdk or a "postgrest filter lib"
get filter() {
return {
column: 'fts',
operator: 'textSearch',
value: `'${this.search}':*`,
}
}
set filter(data) {
const search = this.getFilterSearch(data)
this.search = search || ''
}

getFilterSearch(filter) {
const search = filter?.value.split(':')[0].split("'")[1]
return search
}

render() {
return html`
<form>
Expand All @@ -40,19 +23,23 @@ export default class R4SupabaseFilterSearch extends LitElement {
</form>
`
}

async onInput(event) {
event.preventDefault()
event.stopPropagation()
this.search = event.target.value
const filter = this.search ? this.filter : null
this.dispatchEvent(
new CustomEvent('input', {
bubbles: false,
detail: filter,
detail: {filter: createSearchFilter(this.search), search: this.search},
})
)
}

// extractSearchFilterValue(filter) {
// return filter?.value.split(':')[0].split("'")[1]
// }

// Disable shadow DOM
createRenderRoot() {
return this
Expand Down
27 changes: 6 additions & 21 deletions src/components/r4-supabase-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const {tables} = dbSchema
* @prop {string} value
*/

/*
An interface to create and manage filters for the Supabase SDK (`supabase.from()...`)
Fires @input event whenever any filter is created or changed.
/**
* An interface to create and manage filters for the Supabase SDK (`supabase.from()...`)
* Fires @input event whenever any filter is created or changed.
*/
export default class R4SupabaseFilters extends LitElement {
static properties = {
Expand All @@ -21,27 +21,12 @@ export default class R4SupabaseFilters extends LitElement {
filters: {type: Array, reflect: true, state: true},
}

// constructor() {
// super()
// if (!this.filters) this.filters = []
// }

updated(attr) {
/* always update the list when any attribute change
for some attribute, first clear the existing search query */
// if (attr.get('filters')) {
// console.log('filters changeD?')
// // this.onFilters()
// }
}

// When any filter is set or changed
async onFilters(updatedFilters) {
console.log('onFilters', this.filters, updatedFilters)
// Called when any filter is added, updated or removed
onFilters(updatedFilters) {
this.dispatchEvent(
new CustomEvent('input', {
bubbles: true,
detail: updatedFilters?.filter((filter) => !!filter),
detail: updatedFilters//?.filter((filter) => !!filter),
})
)
}
Expand Down
Loading

0 comments on commit f042472

Please sign in to comment.