Better structure + channel home in LitElement format

This commit is contained in:
Mehdi Benadel
2024-05-23 14:41:11 +02:00
parent 687c4742f7
commit de974eae22
14 changed files with 191 additions and 217 deletions

View File

@ -0,0 +1,326 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { html, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { ptTr } from '../directives/translation'
import './dynamic-table-form'
import './plugin-configuration-row'
import './help-button'
import { Task } from '@lit/task';
import type { ChannelConfiguration } from 'shared/lib/types'
import { ChannelDetailsService } from '../services/channel-details'
import { provide } from '@lit/context'
import { getGlobalStyleSheets } from '../../global-styles'
import { channelConfigurationContext, channelDetailsServiceContext, registerClientOptionsContext } from '../contexts/channel'
@customElement('channel-configuration')
export class ChannelConfigurationElement extends LitElement {
@provide({ context: registerClientOptionsContext })
@property({ attribute: false })
public registerClientOptions: RegisterClientOptions | undefined
@property({ attribute: false })
public channelId: number | undefined
@provide({ context: channelConfigurationContext })
@state()
public _channelConfiguration: ChannelConfiguration | undefined
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService: ChannelDetailsService | undefined
static styles = [
...getGlobalStyleSheets()
];
@state()
public _formStatus: boolean | any = undefined
private _asyncTaskRender = new Task(this, {
task: async ([registerClientOptions], {signal}) => {
if (this.registerClientOptions) {
this._channelDetailsService = new ChannelDetailsService(this.registerClientOptions)
this._channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0)
}
},
args: () => [this.registerClientOptions]
});
private _saveConfig = () => {
if(this._channelDetailsService && this._channelConfiguration) {
this._channelDetailsService.saveOptions(this._channelConfiguration.channel.id, this._channelConfiguration.configuration)
.then((value) => {
this._formStatus = { success: true }
console.log(`Configuration has been updated`)
this.requestUpdate('_formStatus')
})
.catch((error) => {
this._formStatus = error
console.log(`An error occurred : ${JSON.stringify(this._formStatus)}`)
this.requestUpdate('_formStatus')
});
}
}
render = () => {
let tableHeaderList = {
forbiddenWords: {
entries: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
},
regex: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
},
applyToModerators: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
},
label: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
},
reason: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
},
comments: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC)
}
},
quotes: {
messages: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL2),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC2)
},
delay: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC)
}
},
commands: {
command: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_DESC)
},
message: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_DESC)
}
}
}
let tableSchema = {
forbiddenWords: {
entries: {
inputType: 'textarea',
default: ['helloqwesad'],
separator: '\n',
},
regex: {
inputType: 'text',
default: 'helloaxzca',
},
applyToModerators: {
inputType: 'checkbox',
default: true
},
label: {
inputType: 'text',
default: 'helloasx'
},
reason: {
inputType: 'text',
default: 'transphobia',
datalist: ['Racism', 'Sexism', 'Transphobia', 'Bigotry']
},
comments: {
inputType: 'textarea',
default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.`
},
},
quotes: {
messages: {
inputType: 'textarea',
default: ['default message'],
separator: '\n',
},
delay: {
inputType: 'number',
default: 100,
}
},
commands: {
command: {
inputType: 'text',
default: 'default command',
},
message: {
inputType: 'text',
default: 'default message',
}
}
}
return this._asyncTaskRender.render({
complete: () => html`
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel">
<h1>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE)}:
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${this._channelConfiguration?.channel.displayName}</span>
<span>${this._channelConfiguration?.channel.name}</span>
</span>
<help-button .page="documentation/user/streamers/channel">
</help-button>
</h1>
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}</p>
<form livechat-configuration-channel-options role="form">
<div class="row mt-3">
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC, true)}
.helpPage=${"documentation/user/streamers/slow_mode"}>
<div class="form-group">
<label>
<input
type="number"
name="slow_mode_duration"
class="form-control"
min="0"
max="1000"
id="peertube-livechat-slow-mode-duration"
@input=${(event: InputEvent) => {
if (event?.target && this._channelConfiguration)
this._channelConfiguration.configuration.slowMode.duration = Number((event.target as HTMLInputElement).value)
this.requestUpdate('_channelConfiguration')
}
}
value="${this._channelConfiguration?.configuration.slowMode.duration}"
/>
</label>
</div>
</plugin-configuration-row>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}
.helpPage=${"documentation/user/streamers/channel"}>
<div class="form-group">
<label>
<input
type="checkbox"
name="bot"
id="peertube-livechat-bot"
@input=${(event: InputEvent) => {
if (event?.target && this._channelConfiguration)
this._channelConfiguration.configuration.bot.enabled = (event.target as HTMLInputElement).checked
this.requestUpdate('_channelConfiguration')
}
}
.value=${this._channelConfiguration?.configuration.bot.enabled}
?checked=${this._channelConfiguration?.configuration.bot.enabled}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL)}
</label>
</div>
${this._channelConfiguration?.configuration.bot.enabled ?
html`<div class="form-group">
<label for="peertube-livechat-bot-nickname">${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME)}</label>
<input
type="text"
name="bot_nickname"
class="form-control"
id="peertube-livechat-bot-nickname"
@input=${(event: InputEvent) => {
if (event?.target && this._channelConfiguration)
this._channelConfiguration.configuration.bot.nickname = (event.target as HTMLInputElement).value
this.requestUpdate('_channelConfiguration')
}
}
value="${this._channelConfiguration?.configuration.bot.nickname}"
/>
</div>`
: ''
}
</plugin-configuration-row>
${this._channelConfiguration?.configuration.bot.enabled ?
html`<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
.helpPage=${"documentation/user/streamers/bot/forbidden_words"}>
<dynamic-table-form
.header=${tableHeaderList.forbiddenWords}
.schema=${tableSchema.forbiddenWords}
.rows=${this._channelConfiguration?.configuration.bot.forbiddenWords}
@update=${(e: CustomEvent) => {
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.forbiddenWords = e.detail
this.requestUpdate('_channelConfiguration')
}
}
.formName=${'forbidden-words'}>
</dynamic-table-form>
</plugin-configuration-row>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)}
.helpPage=${"documentation/user/streamers/bot/quotes"}>
<dynamic-table-form
.header=${tableHeaderList.quotes}
.schema=${tableSchema.quotes}
.rows=${this._channelConfiguration?.configuration.bot.quotes}
@update=${(e: CustomEvent) => {
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.quotes = e.detail
this.requestUpdate('_channelConfiguration')
}
}
.formName=${'quote'}>
</dynamic-table-form>
</plugin-configuration-row>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)}
.helpPage=${"documentation/user/streamers/bot/commands"}>
<dynamic-table-form
.header=${tableHeaderList.commands}
.schema=${tableSchema.commands}
.rows=${this._channelConfiguration?.configuration.bot.commands}
@update=${(e: CustomEvent) => {
if (this._channelConfiguration) this._channelConfiguration.configuration.bot.commands = e.detail
this.requestUpdate('_channelConfiguration')
}
}
.formName=${'command'}>
</dynamic-table-form>
</plugin-configuration-row>`
: ''
}
<div class="form-group mt-5">
<button type="button" class="orange-button" @click=${this._saveConfig}>${ptTr(LOC_SAVE)}</button>
</div>
${(this._formStatus && this._formStatus.success === undefined) ?
html`<div class="alert alert-warning" role="alert">
An error occurred : ${JSON.stringify(this._formStatus)}
</div>`
: ''
}
${(this._formStatus && this._formStatus.success === true) ?
html`<div class="alert alert-success" role="alert">
Configuration has been updated
</div>`
: ''
}
</form>
</div>`
})
}
}

View File

@ -0,0 +1,86 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { html, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { ptTr } from '../directives/translation'
import './help-button'
import { Task } from '@lit/task';
import type { ChannelLiveChatInfos } from 'shared/lib/types'
import { ChannelDetailsService } from '../services/channel-details'
import { provide } from '@lit/context'
import { getGlobalStyleSheets } from '../../global-styles'
import { channelDetailsServiceContext, registerClientOptionsContext } from '../contexts/channel'
@customElement('channel-home')
export class ChannelHomeElement extends LitElement {
@provide({ context: registerClientOptionsContext })
@property({ attribute: false })
public registerClientOptions: RegisterClientOptions | undefined
@state()
public _channels: ChannelLiveChatInfos[] | undefined
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService: ChannelDetailsService | undefined
static styles = [
...getGlobalStyleSheets()
];
@state()
public _formStatus: boolean | any = undefined
private _asyncTaskRender = new Task(this, {
task: async ([registerClientOptions], {signal}) => {
// Getting the current username in localStorage. Don't know any cleaner way to do.
const username = window.localStorage.getItem('username')
if (!username) {
throw new Error('Can\'t get the current username.')
}
if (this.registerClientOptions) {
this._channelDetailsService = new ChannelDetailsService(this.registerClientOptions)
this._channels = await this._channelDetailsService.fetchUserChannels(username)
}
},
args: () => [this.registerClientOptions]
});
render = () => {
return this._asyncTaskRender.render({
complete: () => html`
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-home">
<h1>
${ptTr(LOC_LIVECHAT_CONFIGURATION_TITLE)}
<help-button .page="documentation/user/streamers/channel">
</help-button>
</h1>
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_DESC)}</p>
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_PLEASE_SELECT)}</p>
<ul class="peertube-plugin-livechat-configuration-home-channels">
${this._channels?.map((channel) => html`
<li>
<a href="${channel.livechatConfigurationUri}">
${channel.avatar ?
html`<img class="avatar channel" src="${channel.avatar.path}">`
:
html`<div class="avatar channel initial gray"></div>`
}
</a>
<div class="peertube-plugin-livechat-configuration-home-info">
<a href="${channel.livechatConfigurationUri}">
<div>${channel.displayName}</div>
<div>${channel.name}</div>
</a>
</div>
</li>
`)}
</ul>
</div>
`
})
}
}

View File

@ -0,0 +1,424 @@
import { css, html, LitElement, nothing, TemplateResult } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import { customElement, property, state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { getGlobalStyleSheets } from '../../global-styles'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
const AddSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>
</svg>`
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
const RemoveSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line>
</svg>`
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
type DynamicTableAcceptedInputTypes = 'textarea'
| 'select'
| 'checkbox'
| 'range'
| 'color'
| 'date'
| 'datetime'
| 'datetime-local'
| 'email'
| 'file'
| 'image'
| 'month'
| 'number'
| 'password'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week'
interface CellDataSchema {
min?: number
max?: number
minlength?: number
maxlength?: number
size?: number
label?: string
options?: { [key: string]: string }
datalist?: DynamicTableAcceptedTypes[]
separator?: string
inputType?: DynamicTableAcceptedInputTypes
default?: DynamicTableAcceptedTypes
}
@customElement('dynamic-table-form')
export class DynamicTableFormElement extends LitElement {
@property({ attribute: false })
public header: { [key: string]: { colName: TemplateResult, description: TemplateResult } } = {}
@property({ attribute: false })
public schema: { [key: string]: CellDataSchema } = {}
@property({ attribute: false })
public rows: { [key: string]: DynamicTableAcceptedTypes }[] = []
@state()
public _rowsById: { _id: number; row: { [key: string]: DynamicTableAcceptedTypes } }[] = []
@property({ attribute: false })
public formName: string = ''
@state()
private _lastRowId = 1
@property({ attribute: false })
private _colOrder: string[] = []
static styles = [
...getGlobalStyleSheets(),
css`
:host table {
table-layout: fixed;
text-align: center;
}
:host table td, table th {
word-wrap:break-word;
vertical-align: top;
padding: 5px 7px;
}
:host table tbody > :nth-child(odd) {
background-color: var(--greySecondaryBackgroundColor);
}
:host button {
padding: 2px;
}
:host .dynamic-table-add-row {
background-color: var(--bs-green);
}
:host .dynamic-table-remove-row {
background-color: var(--bs-orange);
}
`
];
// fixes situations when list has been reinitialized or changed outside of CustomElement
private _updateLastRowId = () => {
for (let rowById of this._rowsById) {
this._lastRowId = Math.max(this._lastRowId, rowById._id + 1);
}
}
private _getDefaultRow = () : { [key: string]: DynamicTableAcceptedTypes } => {
this._updateLastRowId()
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
}
private _addRow = () => {
let newRow = this._getDefaultRow()
this._rowsById.push({_id:this._lastRowId++, row: newRow})
this.rows.push(newRow)
this.requestUpdate('rows')
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
}
private _removeRow = (rowId: number) => {
let rowToRemove = this._rowsById.filter(rowById => rowById._id == rowId).map(rowById => rowById.row)[0]
this._rowsById = this._rowsById.filter((rowById) => rowById._id !== rowId)
this.rows = this.rows.filter((row) => row !== rowToRemove)
this.requestUpdate('rows')
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
}
render = () => {
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-table`
this._updateLastRowId()
this._rowsById.filter(rowById => this.rows.includes(rowById.row))
for (let row of this.rows) {
if (this._rowsById.filter(rowById => rowById.row === row).length == 0) {
this._rowsById.push({_id: this._lastRowId++, row })
}
}
return html`
<table class="table table-striped table-hover table-sm" id=${inputId}>
${this._renderHeader()}
<tbody>
${repeat(this._rowsById, (rowById) => rowById._id, this._renderDataRow)}
</tbody>
${this._renderFooter()}
</table>
`
}
private _renderHeader = () => {
if (this._colOrder.length !== Object.keys(this.header).length) {
this._colOrder = this._colOrder.filter(key => Object.keys(this.header).includes(key))
this._colOrder.push(...Object.keys(this.header).filter(key => !this._colOrder.includes(key)))
}
return html`<thead>
<tr>
${Object.entries(this.header).sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
.map(([k,v]) => this._renderHeaderCell(v))}
<th scope="col"></th>
</tr>
</thead>`
}
private _renderHeaderCell = (headerCellData: { colName: TemplateResult, description: TemplateResult }) => {
return html`<th scope="col">
<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${headerCellData.description}>${headerCellData.colName}</div>
</th>`
}
private _renderDataRow = (rowData: { _id: number; row: {[key: string]: DynamicTableAcceptedTypes} }) => {
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-row-${rowData._id}`
return html`<tr id=${inputId}>
${Object.entries(rowData.row).filter(([k, v]) => k != '_id')
.sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2))
.map((data) => this.renderDataCell(data, rowData._id))}
<td class="form-group"><button class="btn dynamic-table-remove-row" @click=${() => this._removeRow(rowData._id)}>${unsafeHTML(RemoveSVG)}</button></td>
</tr>`
}
private _renderFooter = () => {
return html`<tfoot>
<tr>
${Object.values(this.header).map(() => html`<td></td>`)}
<td><button class="btn dynamic-table-add-row" @click=${this._addRow}>${unsafeHTML(AddSVG)}</button></td>
</tr>
</tfoot>`
}
renderDataCell = (property: [string, DynamicTableAcceptedTypes], rowId: number) => {
const [propertyName, propertyValue] = property
const propertySchema = this.schema[propertyName] ?? {}
let formElement
const inputName = `${this.formName.replaceAll('-', '_')}_${propertyName.toString().replaceAll('-', '_')}_${rowId}`
const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-${propertyName.toString().replaceAll('_', '-')}-${rowId}`
switch (propertyValue.constructor) {
case String:
switch (propertySchema.inputType) {
case undefined:
propertySchema.inputType = 'text'
case 'text':
case 'color':
case 'date':
case 'datetime':
case 'datetime-local':
case 'email':
case 'file':
case 'image':
case 'month':
case 'number':
case 'password':
case 'range':
case 'tel':
case 'time':
case 'url':
case 'week':
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
case 'textarea':
formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
case 'select':
formElement = this._renderSelect(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
}
break
case Date:
switch (propertySchema.inputType) {
case undefined:
propertySchema.inputType = 'datetime'
case 'date':
case 'datetime':
case 'datetime-local':
case 'time':
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, (propertyValue as Date).toISOString())
break
}
break
case Number:
switch (propertySchema.inputType) {
case undefined:
propertySchema.inputType = 'number'
case 'number':
case 'range':
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string)
break
}
break
case Boolean:
switch (propertySchema.inputType) {
case undefined:
propertySchema.inputType = 'checkbox'
case 'checkbox':
formElement = this._renderCheckbox(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as boolean)
break
}
break
case Array:
switch (propertySchema.inputType) {
case undefined:
propertySchema.inputType = 'text'
case 'text':
case 'color':
case 'date':
case 'datetime':
case 'datetime-local':
case 'email':
case 'file':
case 'image':
case 'month':
case 'number':
case 'password':
case 'range':
case 'tel':
case 'time':
case 'url':
case 'week':
formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema,
(propertyValue as Array<number | string>).join(propertySchema.separator ?? ','))
break
case 'textarea':
formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema,
(propertyValue as Array<number | string>).join(propertySchema.separator ?? ','))
break
}
}
if (!formElement) {
console.warn(`value type '${propertyValue.constructor}' is incompatible`
+ `with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`)
}
return html`<td class="form-group">${formElement}</td>`
}
_renderInput = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
return html`<input
type=${propertySchema.inputType}
name=${inputName}
class="form-control"
id=${inputId}
list=${(propertySchema?.datalist) ? inputId + '-datalist' : nothing}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}
/>
${(propertySchema?.datalist) ? html`<datalist id=${inputId + '-datalist'}>
${(propertySchema?.datalist ?? []).map((value) => html`<option value=${value} />`)}
</datalist>` : nothing}
`
}
_renderTextarea = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
return html`<textarea
name=${inputName}
class="form-control"
id=${inputId}
min=${ifDefined(propertySchema?.min)}
max=${ifDefined(propertySchema?.max)}
minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${ifDefined(propertySchema?.maxlength)}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}
></textarea>`
}
_renderCheckbox = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: boolean) => {
return html`<input
type="checkbox"
name=${inputName}
class="form-check-input"
id=${inputId}
@input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}
?checked=${propertyValue}
/>`
}
_renderSelect = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => {
return html`<select
class="form-select"
aria-label="Default select example"
@change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
>
<option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option>
${Object.entries(propertySchema?.options ?? {})
?.map(([value, name]) =>
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
)}
</select>`
}
_updatePropertyFromValue = (event: Event, propertyName: string, propertySchema: CellDataSchema, rowId: number) => {
let target = event?.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)
let value = (target && target instanceof HTMLInputElement && target.type == "checkbox") ? !!(target?.checked) : target?.value
if (value !== undefined) {
for (let rowById of this._rowsById) {
if (rowById._id === rowId) {
switch (rowById.row[propertyName].constructor) {
case Array:
rowById.row[propertyName] = (value as string).split(propertySchema.separator ?? ',')
default:
rowById.row[propertyName] = value
}
this.rows = this._rowsById.map(rowById => rowById.row)
this.requestUpdate('rows')
this.requestUpdate('rowsById')
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
return
}
}
console.warn(`Could not update property : Did not find a property named '${propertyName}' in row '${rowId}'`)
}
else {
console.warn(`Could not update property : Target or value was undefined`)
}
}
}

View File

@ -0,0 +1,50 @@
import { html, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { helpButtonSVG } from '../../../videowatch/buttons'
import { consume } from '@lit/context'
import { registerClientOptionsContext } from '../contexts/channel'
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { Task } from '@lit/task'
import { localizedHelpUrl } from '../../../utils/help'
import { ptTr } from '../directives/translation'
import { DirectiveResult } from 'lit/directive'
import { getGlobalStyleSheets } from '../../global-styles'
@customElement('help-button')
export class HelpButtonElement extends LitElement {
@consume({context: registerClientOptionsContext})
public registerClientOptions: RegisterClientOptions | undefined
@property({ attribute: false })
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
@property({ attribute: false })
public page: string = ''
@state()
public url: URL = new URL('https://lmddgtfy.net/')
static styles = [
...getGlobalStyleSheets()
];
private _asyncTaskRender = new Task(this, {
task: async ([registerClientOptions], {signal}) => {
this.url = new URL(registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: this.page }) : '')
},
args: () => [this.registerClientOptions]
});
render() {
return this._asyncTaskRender.render({
complete: () => html`<a
href="${this.url.href}"
target=_blank
title="${this.buttonTitle}"
class="orange-button peertube-button-link"
>${unsafeHTML(helpButtonSVG())}</a>`
})
}
}

View File

@ -0,0 +1,36 @@
import { html, LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import './help-button'
import { getGlobalStyleSheets } from '../../global-styles'
@customElement('plugin-configuration-row')
export class PluginConfigurationRowElement extends LitElement {
@property({ attribute: false })
public title: string = `title`
@property({ attribute: false })
public description: string = `Here's a description`
@property({ attribute: false })
public helpPage: string = 'documentation'
static styles = [
...getGlobalStyleSheets()
];
render() {
return html`
<div class="row mt-5">
<div class="col-12 col-lg-4 col-xl-3">
<h2>${this.title}</h2>
<p>${this.description}</p>
<help-button .page=${this.helpPage}>
</help-button>
</div>
<div class="col-12 col-lg-8 col-xl-9">
<slot><p>Nothing in this row.</p></slot>
</div>
</div>`
}
}