Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework Settings gui (SOFIE-2300) #922

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meteor/client/lib/EditAttribute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { FloatInputControl } from './Components/FloatInput'
import { joinLines, MultiLineTextInputControl, splitValueIntoLines } from './Components/MultiLineTextInput'
import { JsonTextInputControl, tryParseJson } from './Components/JsonTextInput'

interface IEditAttribute extends IEditAttributeBaseProps {
export interface IEditAttribute extends IEditAttributeBaseProps {
type: EditAttributeType
}
export type EditAttributeType =
Expand Down
157 changes: 157 additions & 0 deletions meteor/client/lib/guiSettings/RenderGUISettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from 'react'
import { verifyGUISettings } from './verifyGUISettings'
import { literal } from '@sofie-automation/corelib/dist/lib'
import { mapAndFilter } from '../../../lib/lib'
import { defaultEditAttributeProps } from './lib'
import { GUISetting, GUISettingBase, GUISettingSection, GUISettings, GUISettingsType } from './guiSettings'
import { TextInputControl } from '../Components/TextInput'

export const RenderGUISettings: React.FC<{ context: GUIRenderContext; settings: GUISettings }> =
function RenderGUISettings({ settings, context }) {
const [filterString, setFilterString] = React.useState('')

const verifyWarning = verifyGUISettings(settings)

const filteredSettings = filterSettings(settings, filterString)

return (
<div className="gui-settings">
<div className="gui-settings-filter">
<TextInputControl
{...defaultEditAttributeProps}
classNames="input text-input input-l"
// placeholder={schema.default}
value={filterString}
handleUpdate={(newValue) => {
setFilterString(newValue)
}}
updateOnKey={true}
/>

{/* <EditAttribute type="text" value={filterString} /> */}
</div>

{verifyWarning && <div className="gui-settings-warning">{verifyWarning}</div>}

<RenderListItemList context={context} settings={filteredSettings.list} />
</div>
)
}
interface GUIRenderContext {
baseURL: string
}

const RenderListItemList: React.FC<{
context: GUIRenderContext
settings: (GUISetting | GUISettingSection)[]
}> = function RenderListItemList({ context, settings }) {
return (
<div className="gui-settings-list">
{settings.map((setting) => (
<div className="gui-settings-list__item" key={setting.id}>
<RenderListItem context={context} setting={setting} />
</div>
))}
</div>
)
}
const RenderListItem: React.FC<{ context: GUIRenderContext; setting: GUISetting | GUISettingSection }> =
function RenderListItem({ context, setting }) {
if (setting.type === GUISettingsType.SECTION) {
return (
<div className="gui-settings-section">
<h2 className="name">
<a href={getDeepLink(setting, context)}>{setting.name}</a>
</h2>
<div className="description">{setting.description}</div>
<RenderListItemList context={context} settings={setting.getList()} />
</div>
)
} else {
const warning = setting.getWarning?.()
return (
<div className="gui-settings-setting">
<h2 className="name">{setting.name}</h2>
<div className="description">{setting.description}</div>
{warning && <div className="warning">{warning}</div>}
<div className="content">
<setting.render />
</div>
</div>
)
}
}
function filterSettings(settings: GUISettings, filterString: string): GUISettings {
if (filterString === '') return settings

const filteredSettings: GUISettings = {
list: mapAndFilter(settings.list, (setting) => filterSetting(setting, filterString)),
}

return filteredSettings
}
function filterSetting(
setting: GUISetting | GUISettingSection,
filterString: string
): GUISetting | GUISettingSection | undefined {
let useSetting = false

if (scatterMatchString(setting.name, filterString) !== null) {
useSetting = true
}
if (!useSetting && setting.description && scatterMatchString(setting.description, filterString) !== null) {
useSetting = true
}
if (!useSetting && scatterMatchString(setting.id, filterString) !== null) {
useSetting = true
}
if (!useSetting) {
const searchString: string =
typeof setting.getSearchString === 'string' ? setting.getSearchString : setting.getSearchString()
if (scatterMatchString(searchString, filterString) !== null) {
useSetting = true
}
}

if (setting.type === GUISettingsType.SECTION) {
const settingsList = mapAndFilter(setting.getList(), (listSetting) => filterSetting(listSetting, filterString))

if (settingsList.length > 0) useSetting = true
if (!useSetting) return undefined

return literal<GUISettingSection>({
...setting,
getList: () => {
return settingsList
},
})
} else {
if (!useSetting) return undefined
return setting
}

return setting
}

function getDeepLink(setting: GUISettingBase, context: GUIRenderContext): string {
return `${context.baseURL}/${setting.id}`
}

/** Returns a number it the search is somewhere in source, for example "johny" matches "Johan Nyman", or null if it's not found */
export function scatterMatchString(source: string, search: string): null | number {
search = search.toLowerCase()
source = source.toLowerCase()

let j = 0
for (const char of search) {
const foundIndex = source.indexOf(char, j)

if (foundIndex === -1) {
// no match
return null
} else {
j = foundIndex + 1
}
}
return j
}
33 changes: 33 additions & 0 deletions meteor/client/lib/guiSettings/guiSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export interface GUISettings {
list: (GUISetting | GUISettingSection)[]
}
export enum GUISettingsType {
SECTION = 'section',
SETTING = 'setting',
}
export interface GUISettingBase {
type: GUISettingsType
name: string
description?: string
/**
* Globally unique identifier, used to deep-link to a certain setting.
* Must be on the form: "my/deep/link"
*/
id: string
/** @returns a string, used when filtering. Normally contains displayed and non-displayed data, such as current-value, options etc.. */
getSearchString: string | (() => string)
}
export interface GUISettingSection extends GUISettingBase {
type: GUISettingsType.SECTION
getList: () => (GUISetting | GUISettingSection)[]
/** @returns component to display when section is folded/closed in GUI */
renderSummary?: React.FC<{}>
}

export interface GUISetting extends GUISettingBase {
type: GUISettingsType.SETTING
/** @returns the component to use to render the setting */
render: React.FC<{}>
/** @returns a warning if there is an issue with the setting, otherwise falsy */
getWarning?: () => string | undefined | null | false
}
24 changes: 24 additions & 0 deletions meteor/client/lib/guiSettings/lib.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { IEditAttribute } from '../EditAttribute'
import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { useHistory } from 'react-router-dom'

export const defaultEditAttributeProps: Partial<IEditAttribute> = {
modifiedClassName: 'bghl',
className: 'mdinput',
}

export const RedirectToShowStyleButton = React.memo(function RedirectToShowStyleButton(props: {
id: ShowStyleBaseId
name: string
}) {
const history = useHistory()

const doRedirect = () => history.push('/settings/showStyleBase/' + props.id)

return (
<button className="btn btn-primary btn-add-new" onClick={doRedirect}>
Edit {props.name}
</button>
)
})
Loading