Refactoring: moving some template to separate files + classMap fix.

This commit is contained in:
John Livingston 2024-06-12 17:08:48 +02:00
parent 2c3739f633
commit 4976a4f282
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 518 additions and 438 deletions

View File

@ -4,16 +4,16 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelConfiguration } from 'shared/lib/types' import type { ChannelConfiguration } from 'shared/lib/types'
import { ChannelDetailsService } from '../services/channel-details'
import { channelConfigurationContext, channelDetailsServiceContext } from '../contexts/channel'
import { LivechatElement } from '../../lib/elements/livechat'
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
import { tplChannelConfiguration } from './templates/channel-configuration'
import { TemplateResult, html, nothing } from 'lit' import { TemplateResult, html, nothing } from 'lit'
import { customElement, property, state } from 'lit/decorators.js' import { customElement, property, state } from 'lit/decorators.js'
import { ptTr } from '../../lib/directives/translation' import { ptTr } from '../../lib/directives/translation'
import { Task } from '@lit/task' import { Task } from '@lit/task'
import { ChannelDetailsService } from '../services/channel-details'
import { provide } from '@lit/context' import { provide } from '@lit/context'
import { channelConfigurationContext, channelDetailsServiceContext } from '../contexts/channel'
import { LivechatElement } from '../../lib/elements/livechat'
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
import { classMap } from 'lit/directives/class-map.js'
@customElement('livechat-channel-configuration') @customElement('livechat-channel-configuration')
export class ChannelConfigurationElement extends LivechatElement { export class ChannelConfigurationElement extends LivechatElement {
@ -22,16 +22,16 @@ export class ChannelConfigurationElement extends LivechatElement {
@provide({ context: channelConfigurationContext }) @provide({ context: channelConfigurationContext })
@state() @state()
public _channelConfiguration?: ChannelConfiguration public channelConfiguration?: ChannelConfiguration
@provide({ context: channelDetailsServiceContext }) @provide({ context: channelDetailsServiceContext })
private _channelDetailsService?: ChannelDetailsService private _channelDetailsService?: ChannelDetailsService
@state() @state()
public _validationError?: ValidationError public validationError?: ValidationError
@state() @state()
private _actionDisabled: boolean = false public actionDisabled: boolean = false
private _asyncTaskRender: Task private _asyncTaskRender: Task
@ -44,37 +44,44 @@ export class ChannelConfigurationElement extends LivechatElement {
return new Task(this, { return new Task(this, {
task: async () => { task: async () => {
this._channelDetailsService = new ChannelDetailsService(this.ptOptions) this._channelDetailsService = new ChannelDetailsService(this.ptOptions)
this._channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0) this.channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0)
this._actionDisabled = false // in case of reset this.actionDisabled = false // in case of reset
}, },
args: () => [] args: () => []
}) })
} }
private async _reset (event?: Event): Promise<void> { /**
* Resets the form by reloading data from backend.
*/
public async reset (event?: Event): Promise<void> {
event?.preventDefault() event?.preventDefault()
this._actionDisabled = true this.actionDisabled = true
this._asyncTaskRender = this._initTask() this._asyncTaskRender = this._initTask()
this.requestUpdate() this.requestUpdate()
} }
private readonly _saveConfig = async (event?: Event): Promise<void> => { /**
* Saves the channel configuration.
* @param event event
*/
public readonly saveConfig = async (event?: Event): Promise<void> => {
event?.preventDefault() event?.preventDefault()
if (this._channelDetailsService && this._channelConfiguration) { if (this._channelDetailsService && this.channelConfiguration) {
this._actionDisabled = true this.actionDisabled = true
this._channelDetailsService.saveOptions(this._channelConfiguration.channel.id, this._channelDetailsService.saveOptions(this.channelConfiguration.channel.id,
this._channelConfiguration.configuration) this.channelConfiguration.configuration)
.then(() => { .then(() => {
this._validationError = undefined this.validationError = undefined
this.ptTranslate(LOC_SUCCESSFULLY_SAVED).then((msg) => { this.ptTranslate(LOC_SUCCESSFULLY_SAVED).then((msg) => {
this.ptNotifier.info(msg) this.ptNotifier.info(msg)
}, () => {}) }, () => {})
this.requestUpdate('_validationError') this.requestUpdate('_validationError')
}) })
.catch(async (error: Error) => { .catch(async (error: Error) => {
this._validationError = undefined this.validationError = undefined
if (error instanceof ValidationError) { if (error instanceof ValidationError) {
this._validationError = error this.validationError = error
} }
console.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`) console.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`)
this.ptNotifier.error( this.ptNotifier.error(
@ -85,22 +92,22 @@ export class ChannelConfigurationElement extends LivechatElement {
this.requestUpdate('_validationError') this.requestUpdate('_validationError')
}) })
.finally(() => { .finally(() => {
this._actionDisabled = false this.actionDisabled = false
}) })
} }
} }
private readonly _getInputValidationClass = (propertyName: string): { [key: string]: boolean } => { public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => {
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this._validationError?.properties[`${propertyName}`] this.validationError?.properties[`${propertyName}`]
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {} return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
} }
private readonly _renderFeedback = (feedbackId: string, public readonly renderFeedback = (feedbackId: string,
propertyName: string): TemplateResult | typeof nothing => { propertyName: string): TemplateResult | typeof nothing => {
const errorMessages: TemplateResult[] = [] const errorMessages: TemplateResult[] = []
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this._validationError?.properties[`${propertyName}`] ?? undefined this.validationError?.properties[`${propertyName}`] ?? undefined
if (validationErrorTypes && validationErrorTypes.length !== 0) { if (validationErrorTypes && validationErrorTypes.length !== 0) {
if (validationErrorTypes.includes(ValidationErrorType.WrongType)) { if (validationErrorTypes.includes(ValidationErrorType.WrongType)) {
@ -120,283 +127,10 @@ export class ChannelConfigurationElement extends LivechatElement {
} }
protected override render = (): unknown => { protected override render = (): unknown => {
const tableHeaderList = {
forbiddenWords: {
entries: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
},
regexp: {
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)
}
}
}
const tableSchema = {
forbiddenWords: {
entries: {
inputType: 'tags',
default: [],
separators: ['\n', '\t', ';']
},
regexp: {
inputType: 'checkbox',
default: false
},
applyToModerators: {
inputType: 'checkbox',
default: false
},
label: {
inputType: 'text',
default: ''
},
reason: {
inputType: 'text',
default: '',
datalist: []
},
comments: {
inputType: 'textarea',
default: ''
}
},
quotes: {
messages: {
inputType: 'tags',
default: [],
separators: ['\n', '\t', ';']
},
delay: {
inputType: 'number',
default: 10
}
},
commands: {
command: {
inputType: 'text',
default: ''
},
message: {
inputType: 'text',
default: ''
}
}
}
return this._asyncTaskRender.render({ return this._asyncTaskRender.render({
pending: () => html`<livechat-spinner></livechat-spinner>`, pending: () => html`<livechat-spinner></livechat-spinner>`,
error: () => html`<livechat-error></livechat-error>`, error: () => html`<livechat-error></livechat-error>`,
complete: () => html` complete: () => tplChannelConfiguration(this)
<div class="margin-content peertube-plugin-livechat-configuration
peertube-plugin-livechat-configuration-channel">
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${this._channelConfiguration?.channel.displayName}</span>
<span>${this._channelConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'configuration'} .channelId=${this.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/channel'}>
</livechat-help-button>
</p>
<form livechat-configuration-channel-options role="form" @submit=${this._saveConfig}>
<livechat-configuration-section-header
.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'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
<input
type="number"
name="slowmode_duration"
class="form-control ${classMap(this._getInputValidationClass('slowMode.duration'))}"
min="0"
max="1000"
id="peertube-livechat-slowmode-duration"
aria-describedby="peertube-livechat-slowmode-duration-feedback"
@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>
${this._renderFeedback('peertube-livechat-slowmode-duration-feedback', 'slowMode.duration')}
</div>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}
.helpPage=${'documentation/user/streamers/channel'}>
</livechat-configuration-section-header>
<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="1"
?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 ${classMap(this._getInputValidationClass('bot.nickname'))}"
id="peertube-livechat-bot-nickname"
aria-describedby="peertube-livechat-bot-nickname-feedback"
@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 ?? ''}"
/>
${this._renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
</div>
<livechat-configuration-section-header
.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'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.forbiddenWords}
.schema=${tableSchema.forbiddenWords}
.validation=${this._validationError?.properties}
.validationPrefix=${'bot.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'}
></livechat-dynamic-table-form>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)}
.helpPage=${'documentation/user/streamers/bot/quotes'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.quotes}
.schema=${tableSchema.quotes}
.validation=${this._validationError?.properties}
.validationPrefix=${'bot.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'}
></livechat-dynamic-table-form>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)}
.helpPage=${'documentation/user/streamers/bot/commands'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.commands}
.schema=${tableSchema.commands}
.validation=${this._validationError?.properties}
.validationPrefix=${'bot.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'}
></livechat-dynamic-table-form>
`}
<div class="form-group mt-5">
<button type="reset" @click=${this._reset} ?disabled=${this._actionDisabled}>
${ptTr(LOC_CANCEL)}
</button>
<button type="submit" ?disabled=${this._actionDisabled}>
${ptTr(LOC_SAVE)}
</button>
</div>
</form>
</div>`
}) })
} }
} }

View File

@ -3,13 +3,11 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelEmojisConfiguration } from 'shared/lib/types' import type { ChannelEmojisConfiguration } from 'shared/lib/types'
import type { DynamicFormHeader, DynamicFormSchema } from '../../lib/elements/dynamic-table-form'
import { LivechatElement } from '../../lib/elements/livechat' import { LivechatElement } from '../../lib/elements/livechat'
import { ChannelDetailsService } from '../services/channel-details' import { ChannelDetailsService } from '../services/channel-details'
import { channelDetailsServiceContext } from '../contexts/channel' import { channelDetailsServiceContext } from '../contexts/channel'
import { maxEmojisPerChannel } from 'shared/lib/emojis'
import { ptTr } from '../../lib/directives/translation'
import { ValidationError } from '../../lib/models/validation' import { ValidationError } from '../../lib/models/validation'
import { tplChannelEmojis } from './templates/channel-emojis'
import { Task } from '@lit/task' import { Task } from '@lit/task'
import { customElement, property, state } from 'lit/decorators.js' import { customElement, property, state } from 'lit/decorators.js'
import { provide } from '@lit/context' import { provide } from '@lit/context'
@ -23,16 +21,16 @@ export class ChannelEmojisElement extends LivechatElement {
@property({ attribute: false }) @property({ attribute: false })
public channelId?: number public channelId?: number
private _channelEmojisConfiguration?: ChannelEmojisConfiguration public channelEmojisConfiguration?: ChannelEmojisConfiguration
@provide({ context: channelDetailsServiceContext }) @provide({ context: channelDetailsServiceContext })
private _channelDetailsService?: ChannelDetailsService private _channelDetailsService?: ChannelDetailsService
@state() @state()
private _validationError?: ValidationError public validationError?: ValidationError
@state() @state()
private _actionDisabled: boolean = false public actionDisabled: boolean = false
private _asyncTaskRender: Task private _asyncTaskRender: Task
@ -42,111 +40,10 @@ export class ChannelEmojisElement extends LivechatElement {
} }
protected override render = (): unknown => { protected override render = (): unknown => {
const tableHeaderList: DynamicFormHeader = {
sn: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME),
description: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME_DESC),
headerClassList: ['peertube-livechat-emojis-col-sn']
},
url: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_FILE),
description: ptTr(LOC_LIVECHAT_EMOJIS_FILE_DESC),
headerClassList: ['peertube-livechat-emojis-col-file']
}
}
const tableSchema: DynamicFormSchema = {
sn: {
inputType: 'text',
default: ''
},
url: {
inputType: 'image-file',
default: '',
colClassList: ['peertube-livechat-emojis-col-file']
}
}
return this._asyncTaskRender.render({ return this._asyncTaskRender.render({
pending: () => html`<livechat-spinner></livechat-spinner>`, pending: () => html`<livechat-spinner></livechat-spinner>`,
error: () => html`<livechat-error></livechat-error>`, error: () => html`<livechat-error></livechat-error>`,
complete: () => html` complete: () => tplChannelEmojis(this)
<div
class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel"
>
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${this._channelEmojisConfiguration?.channel.displayName}</span>
<span>${this._channelEmojisConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'emojis'} .channelId=${this.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
</livechat-help-button>
</p>
<form role="form" @submit=${this._saveEmojis}>
<div class="peertube-plugin-livechat-configuration-actions">
${
this._channelEmojisConfiguration?.emojis?.customEmojis?.length
? html`
<button
@click=${this._exportEmojis}
?disabled=${this._actionDisabled}
>
${ptTr(LOC_ACTION_EXPORT)}
</button>`
: ''
}
${
(this._channelEmojisConfiguration?.emojis?.customEmojis?.length ?? 0) < maxEmojisPerChannel
? html`
<button
@click=${this._importEmojis}
?disabled=${this._actionDisabled}
>
${ptTr(LOC_ACTION_IMPORT)}
</button>`
: ''
}
</div>
<livechat-dynamic-table-form
.header=${tableHeaderList}
.schema=${tableSchema}
.maxLines=${maxEmojisPerChannel}
.validation=${this._validationError?.properties}
.validationPrefix=${'emojis'}
.rows=${this._channelEmojisConfiguration?.emojis.customEmojis}
@update=${(e: CustomEvent) => {
if (this._channelEmojisConfiguration) {
this._channelEmojisConfiguration.emojis.customEmojis = e.detail
// Fixing missing ':' for shortnames:
for (const desc of this._channelEmojisConfiguration.emojis.customEmojis) {
if (desc.sn === '') { continue }
if (!desc.sn.startsWith(':')) { desc.sn = ':' + desc.sn }
if (!desc.sn.endsWith(':')) { desc.sn += ':' }
}
this.requestUpdate('_channelEmojisConfiguration')
}
}
}
></livechat-dynamic-table-form>
<div class="form-group mt-5">
<button type="reset" @click=${this._reset} ?disabled=${this._actionDisabled}>
${ptTr(LOC_CANCEL)}
</button>
<button type="submit" ?disabled=${this._actionDisabled}>
${ptTr(LOC_SAVE)}
</button>
</div>
</form>
</div>
`
}) })
} }
@ -157,39 +54,46 @@ export class ChannelEmojisElement extends LivechatElement {
throw new Error('Missing channelId') throw new Error('Missing channelId')
} }
this._channelDetailsService = new ChannelDetailsService(this.ptOptions) this._channelDetailsService = new ChannelDetailsService(this.ptOptions)
this._channelEmojisConfiguration = await this._channelDetailsService.fetchEmojisConfiguration(this.channelId) this.channelEmojisConfiguration = await this._channelDetailsService.fetchEmojisConfiguration(this.channelId)
this._actionDisabled = false // in case of reset this.actionDisabled = false // in case of reset
}, },
args: () => [] args: () => []
}) })
} }
private async _reset (ev?: Event): Promise<void> { /**
* Resets the page, by reloading data from backend.
*/
public async reset (ev?: Event): Promise<void> {
ev?.preventDefault() ev?.preventDefault()
this._actionDisabled = true this.actionDisabled = true
this._asyncTaskRender = this._initTask() this._asyncTaskRender = this._initTask()
this.requestUpdate() this.requestUpdate()
} }
private async _saveEmojis (ev?: Event): Promise<void> { /**
* Saves the emojis form.
* @param ev event
*/
public async saveEmojis (ev?: Event): Promise<void> {
ev?.preventDefault() ev?.preventDefault()
if (!this._channelDetailsService || !this._channelEmojisConfiguration || !this.channelId) { if (!this._channelDetailsService || !this.channelEmojisConfiguration || !this.channelId) {
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR)) this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
return return
} }
try { try {
this._actionDisabled = true this.actionDisabled = true
await this._channelDetailsService.saveEmojisConfiguration(this.channelId, this._channelEmojisConfiguration.emojis) await this._channelDetailsService.saveEmojisConfiguration(this.channelId, this.channelEmojisConfiguration.emojis)
this._validationError = undefined this.validationError = undefined
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED)) this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
this.requestUpdate('_validationError') this.requestUpdate('_validationError')
} catch (error) { } catch (error) {
this._validationError = undefined this.validationError = undefined
let msg: string let msg: string
if ((error instanceof ValidationError)) { if ((error instanceof ValidationError)) {
this._validationError = error this.validationError = error
if (error.message) { if (error.message) {
msg = error.message msg = error.message
} }
@ -198,13 +102,16 @@ export class ChannelEmojisElement extends LivechatElement {
this.ptNotifier.error(msg) this.ptNotifier.error(msg)
this.requestUpdate('_validationError') this.requestUpdate('_validationError')
} finally { } finally {
this._actionDisabled = false this.actionDisabled = false
} }
} }
private async _importEmojis (ev: Event): Promise<void> { /**
* Import emojis action.
*/
public async importEmojis (ev: Event): Promise<void> {
ev.preventDefault() ev.preventDefault()
this._actionDisabled = true this.actionDisabled = true
try { try {
// download a json file: // download a json file:
const file = await new Promise<File>((resolve, reject) => { const file = await new Promise<File>((resolve, reject) => {
@ -266,10 +173,10 @@ export class ChannelEmojisElement extends LivechatElement {
if (entry.isCategoryEmoji === true) { if (entry.isCategoryEmoji === true) {
item.isCategoryEmoji = true item.isCategoryEmoji = true
} }
this._channelEmojisConfiguration?.emojis.customEmojis.push(item) this.channelEmojisConfiguration?.emojis.customEmojis.push(item)
} }
this.requestUpdate('_channelEmojisConfiguration') this.requestUpdate('channelEmojisConfiguration')
this.ptNotifier.info( this.ptNotifier.info(
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO) await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
@ -277,16 +184,19 @@ export class ChannelEmojisElement extends LivechatElement {
} catch (err: any) { } catch (err: any) {
this.ptNotifier.error(err.toString()) this.ptNotifier.error(err.toString())
} finally { } finally {
this._actionDisabled = false this.actionDisabled = false
} }
} }
private async _exportEmojis (ev: Event): Promise<void> { /**
* Export emojis action.
*/
public async exportEmojis (ev: Event): Promise<void> {
ev.preventDefault() ev.preventDefault()
this._actionDisabled = true this.actionDisabled = true
try { try {
const result: ChannelEmojisConfiguration['emojis']['customEmojis'] = [] const result: ChannelEmojisConfiguration['emojis']['customEmojis'] = []
for (const ed of this._channelEmojisConfiguration?.emojis?.customEmojis ?? []) { for (const ed of this.channelEmojisConfiguration?.emojis?.customEmojis ?? []) {
if (!ed.sn || !ed.url) { continue } if (!ed.sn || !ed.url) { continue }
// Here url can be: // Here url can be:
// * the dataUrl representation of a newly uploaded file // * the dataUrl representation of a newly uploaded file
@ -313,10 +223,15 @@ export class ChannelEmojisElement extends LivechatElement {
console.error(err) console.error(err)
this.ptNotifier.error(err.toString()) this.ptNotifier.error(err.toString())
} finally { } finally {
this._actionDisabled = false this.actionDisabled = false
} }
} }
/**
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
* @param url the url
* @returns A dataUrl representation of the image.
*/
private async _convertImageToDataUrl (url: string): Promise<string> { private async _convertImageToDataUrl (url: string): Promise<string> {
if (url.startsWith('data:')) { return url } if (url.startsWith('data:')) { return url }
// There is a trick to convert img to dataUrl: using a canvas. // There is a trick to convert img to dataUrl: using a canvas.

View File

@ -20,9 +20,6 @@ export class ChannelHomeElement extends LivechatElement {
@provide({ context: channelDetailsServiceContext }) @provide({ context: channelDetailsServiceContext })
private _channelDetailsService?: ChannelDetailsService private _channelDetailsService?: ChannelDetailsService
@state()
public _formStatus: boolean | any = undefined
private readonly _asyncTaskRender = new Task(this, { private readonly _asyncTaskRender = new Task(this, {
task: async () => { task: async () => {
// Getting the current username in localStorage. Don't know any cleaner way to do. // Getting the current username in localStorage. Don't know any cleaner way to do.

View File

@ -0,0 +1,296 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelConfigurationElement } from '../channel-configuration'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { ptTr } from '../../../lib/directives/translation'
import { html, TemplateResult } from 'lit'
import { classMap } from 'lit/directives/class-map.js'
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
const tableHeaderList: {[key: string]: DynamicFormHeader} = {
forbiddenWords: {
entries: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
},
regexp: {
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)
}
}
}
const tableSchema: {[key: string]: DynamicFormSchema} = {
forbiddenWords: {
entries: {
inputType: 'tags',
default: [],
separators: ['\n', '\t', ';']
},
regexp: {
inputType: 'checkbox',
default: false
},
applyToModerators: {
inputType: 'checkbox',
default: false
},
label: {
inputType: 'text',
default: ''
},
reason: {
inputType: 'text',
default: '',
datalist: []
},
comments: {
inputType: 'textarea',
default: ''
}
},
quotes: {
messages: {
inputType: 'tags',
default: [],
separators: ['\n', '\t', ';']
},
delay: {
inputType: 'number',
default: 10
}
},
commands: {
command: {
inputType: 'text',
default: ''
},
message: {
inputType: 'text',
default: ''
}
}
}
return html`
<div class="margin-content peertube-plugin-livechat-configuration
peertube-plugin-livechat-configuration-channel">
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${el.channelConfiguration?.channel.displayName}</span>
<span>${el.channelConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'configuration'} .channelId=${el.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/channel'}>
</livechat-help-button>
</p>
<form livechat-configuration-channel-options role="form" @submit=${el.saveConfig}>
<livechat-configuration-section-header
.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'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
<input
type="number"
name="slowmode_duration"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('slowMode.duration')
)
)}
min="0"
max="1000"
id="peertube-livechat-slowmode-duration"
aria-describedby="peertube-livechat-slowmode-duration-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.slowMode.duration =
Number((event.target as HTMLInputElement).value)
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.slowMode.duration ?? ''}"
/>
</label>
${el.renderFeedback('peertube-livechat-slowmode-duration-feedback', 'slowMode.duration')}
</div>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}
.helpPage=${'documentation/user/streamers/channel'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
<input
type="checkbox"
name="bot"
id="peertube-livechat-bot"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.enabled =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.enabled}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL)}
</label>
</div>
${!el.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=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.nickname')
)
)}
id="peertube-livechat-bot-nickname"
aria-describedby="peertube-livechat-bot-nickname-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.nickname =
(event.target as HTMLInputElement).value
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.nickname ?? ''}"
/>
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
</div>
<livechat-configuration-section-header
.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'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.forbiddenWords}
.schema=${tableSchema.forbiddenWords}
.validation=${el.validationError?.properties}
.validationPrefix=${'bot.forbiddenWords'}
.rows=${el.channelConfiguration?.configuration.bot.forbiddenWords}
@update=${(e: CustomEvent) => {
if (el.channelConfiguration) {
el.channelConfiguration.configuration.bot.forbiddenWords = e.detail
el.requestUpdate('channelConfiguration')
}
}
}
.formName=${'forbidden-words'}
></livechat-dynamic-table-form>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)}
.helpPage=${'documentation/user/streamers/bot/quotes'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.quotes}
.schema=${tableSchema.quotes}
.validation=${el.validationError?.properties}
.validationPrefix=${'bot.quotes'}
.rows=${el.channelConfiguration?.configuration.bot.quotes}
@update=${(e: CustomEvent) => {
if (el.channelConfiguration) {
el.channelConfiguration.configuration.bot.quotes = e.detail
el.requestUpdate('channelConfiguration')
}
}
}
.formName=${'quote'}
></livechat-dynamic-table-form>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)}
.helpPage=${'documentation/user/streamers/bot/commands'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.commands}
.schema=${tableSchema.commands}
.validation=${el.validationError?.properties}
.validationPrefix=${'bot.commands'}
.rows=${el.channelConfiguration?.configuration.bot.commands}
@update=${(e: CustomEvent) => {
if (el.channelConfiguration) {
el.channelConfiguration.configuration.bot.commands = e.detail
el.requestUpdate('channelConfiguration')
}
}
}
.formName=${'command'}
></livechat-dynamic-table-form>
`}
<div class="form-group mt-5">
<button type="reset" @click=${el.reset} ?disabled=${el.actionDisabled}>
${ptTr(LOC_CANCEL)}
</button>
<button type="submit" ?disabled=${el.actionDisabled}>
${ptTr(LOC_SAVE)}
</button>
</div>
</form>
</div>`
}

View File

@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelEmojisElement } from '../channel-emojis'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { maxEmojisPerChannel } from 'shared/lib/emojis'
import { ptTr } from '../../../lib/directives/translation'
import { html, TemplateResult } from 'lit'
export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
const tableHeaderList: DynamicFormHeader = {
sn: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME),
description: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME_DESC),
headerClassList: ['peertube-livechat-emojis-col-sn']
},
url: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_FILE),
description: ptTr(LOC_LIVECHAT_EMOJIS_FILE_DESC),
headerClassList: ['peertube-livechat-emojis-col-file']
}
}
const tableSchema: DynamicFormSchema = {
sn: {
inputType: 'text',
default: ''
},
url: {
inputType: 'image-file',
default: '',
colClassList: ['peertube-livechat-emojis-col-file']
}
}
return html`
<div
class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel"
>
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${el.channelEmojisConfiguration?.channel.displayName}</span>
<span>${el.channelEmojisConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
</livechat-help-button>
</p>
<form role="form" @submit=${el.saveEmojis}>
<div class="peertube-plugin-livechat-configuration-actions">
${
el.channelEmojisConfiguration?.emojis?.customEmojis?.length
? html`
<button
@click=${el.exportEmojis}
?disabled=${el.actionDisabled}
>
${ptTr(LOC_ACTION_EXPORT)}
</button>`
: ''
}
${
(el.channelEmojisConfiguration?.emojis?.customEmojis?.length ?? 0) < maxEmojisPerChannel
? html`
<button
@click=${el.importEmojis}
?disabled=${el.actionDisabled}
>
${ptTr(LOC_ACTION_IMPORT)}
</button>`
: ''
}
</div>
<livechat-dynamic-table-form
.header=${tableHeaderList}
.schema=${tableSchema}
.maxLines=${maxEmojisPerChannel}
.validation=${el.validationError?.properties}
.validationPrefix=${'emojis'}
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis}
@update=${(e: CustomEvent) => {
if (el.channelEmojisConfiguration) {
el.channelEmojisConfiguration.emojis.customEmojis = e.detail
// Fixing missing ':' for shortnames:
for (const desc of el.channelEmojisConfiguration.emojis.customEmojis) {
if (desc.sn === '') { continue }
if (!desc.sn.startsWith(':')) { desc.sn = ':' + desc.sn }
if (!desc.sn.endsWith(':')) { desc.sn += ':' }
}
el.requestUpdate('channelEmojisConfiguration')
}
}
}
></livechat-dynamic-table-form>
<div class="form-group mt-5">
<button type="reset" @click=${el.reset} ?disabled=${el.actionDisabled}>
${ptTr(LOC_CANCEL)}
</button>
<button type="submit" ?disabled=${el.actionDisabled}>
${ptTr(LOC_SAVE)}
</button>
</div>
</form>
</div>`
}

View File

@ -7,7 +7,7 @@ import { customElement, property } from 'lit/decorators.js'
import { LivechatElement } from './livechat' import { LivechatElement } from './livechat'
@customElement('livechat-configuration-section-header') @customElement('livechat-configuration-section-header')
export class ConfigurationRowElement extends LivechatElement { export class ConfigurationSectionHeaderElement extends LivechatElement {
@property({ attribute: false }) @property({ attribute: false })
public override title: string = 'title' public override title: string = 'title'

View File

@ -522,7 +522,12 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<input return html`<input
type=${propertySchema.inputType} type=${propertySchema.inputType}
name=${inputName} name=${inputName}
class="form-control ${classMap(this._getInputValidationClass(propertyName, originalIndex))}" class=${classMap(
Object.assign(
{ 'form-control': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId} id=${inputId}
aria-describedby="${inputId}-feedback" aria-describedby="${inputId}-feedback"
list=${(propertySchema.datalist) ? inputId + '-datalist' : nothing} list=${(propertySchema.datalist) ? inputId + '-datalist' : nothing}
@ -550,7 +555,12 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<livechat-tags-input return html`<livechat-tags-input
.type=${'text'} .type=${'text'}
.name=${inputName} .name=${inputName}
class="form-control ${classMap(this._getInputValidationClass(propertyName, originalIndex))}" class=${classMap(
Object.assign(
{ 'form-control': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId} id=${inputId}
.inputPlaceholder=${ifDefined(propertySchema.label)} .inputPlaceholder=${ifDefined(propertySchema.label)}
aria-describedby="${inputId}-feedback" aria-describedby="${inputId}-feedback"
@ -573,7 +583,12 @@ export class DynamicTableFormElement extends LivechatElement {
originalIndex: number): TemplateResult => { originalIndex: number): TemplateResult => {
return html`<textarea return html`<textarea
name=${inputName} name=${inputName}
class="form-control ${classMap(this._getInputValidationClass(propertyName, originalIndex))}" class=${classMap(
Object.assign(
{ 'form-control': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId} id=${inputId}
aria-describedby="${inputId}-feedback" aria-describedby="${inputId}-feedback"
min=${ifDefined(propertySchema.min)} min=${ifDefined(propertySchema.min)}
@ -594,7 +609,12 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<input return html`<input
type="checkbox" type="checkbox"
name=${inputName} name=${inputName}
class="form-check-input ${classMap(this._getInputValidationClass(propertyName, originalIndex))}" class=${classMap(
Object.assign(
{ 'form-check-input': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId} id=${inputId}
aria-describedby="${inputId}-feedback" aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)} @change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
@ -610,7 +630,12 @@ export class DynamicTableFormElement extends LivechatElement {
propertyValue: string, propertyValue: string,
originalIndex: number): TemplateResult => { originalIndex: number): TemplateResult => {
return html`<select return html`<select
class="form-select ${classMap(this._getInputValidationClass(propertyName, originalIndex))}" class=${classMap(
Object.assign(
{ 'form-select': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId} id=${inputId}
aria-describedby="${inputId}-feedback" aria-describedby="${inputId}-feedback"
aria-label=${inputName} aria-label=${inputName}
@ -634,7 +659,7 @@ export class DynamicTableFormElement extends LivechatElement {
): TemplateResult => { ): TemplateResult => {
return html`<livechat-image-file-input return html`<livechat-image-file-input
.name=${inputName} .name=${inputName}
class="${classMap(this._getInputValidationClass(propertyName, originalIndex))}" class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
id=${inputId} id=${inputId}
aria-describedby="${inputId}-feedback" aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)} @change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}