diff --git a/README.md b/README.md index dba87e7..4f82401 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ settings: override_timezone: '' refresh_interval: '30' screenly_oauth_tokens_url: 'http://localhost:3000/' - stack_field: '' view_id: '' ``` @@ -82,16 +81,15 @@ screenly edge-app instance create ## Configuration -| Setting | Type | Required | Description | -| ------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `access_token` | secret | No | For testing only. In production, the access token is fetched dynamically via the Screenly OAuth service. | -| `base_id` | string | Yes | Airtable base ID (e.g. `appXXXXXXXXXXXXXX`) | -| `display_errors` | string | No | Display errors on screen for debugging (`true`/`false`). Default: `false` | -| `override_locale` | string | No | Override the locale for date formatting (e.g. `en-US`, `fr`). Defaults to GPS-based detection. | -| `override_timezone` | string | No | Override the timezone for date formatting (e.g. `America/New_York`). Defaults to GPS-based detection. | -| `refresh_interval` | string | No | How often (in seconds) to refresh Airtable data. Default: `30` | -| `stack_field` | string | No | For kanban views: name of the single-select field to stack by (e.g. `Status`). Must be a single-select field. Falls back to the first single-select field if not set. | -| `view_id` | string | Yes | Airtable view ID (e.g. `viwXXXXXXXXXXXXXX`). Supports grid and kanban views. | +| Setting | Type | Required | Description | +| ------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------- | +| `access_token` | secret | No | For testing only. In production, the access token is fetched dynamically via the Screenly OAuth service. | +| `base_id` | string | Yes | Airtable base. Selected via dropdown in the Screenly dashboard. | +| `display_errors` | string | No | Display errors on screen for debugging (`true`/`false`). Default: `false` | +| `override_locale` | string | No | Override the locale for date formatting (e.g. `en-US`, `fr`). Defaults to GPS-based detection. | +| `override_timezone` | string | No | Override the timezone for date formatting (e.g. `America/New_York`). Defaults to GPS-based detection. | +| `refresh_interval` | string | No | How often (in seconds) to refresh Airtable data. Default: `30` | +| `view_id` | string | Yes | Airtable view. Selected via dropdown in the Screenly dashboard. Supports grid and kanban views. | ## Limitations @@ -101,9 +99,10 @@ screenly edge-app instance create This app uses the Screenly OAuth service to obtain an Airtable access token at runtime. For local development, the `mock-server` acts as a stand-in for that service. -## Finding Your IDs +## Finding Your IDs (Local Development) -Navigate to your view in Airtable. The base ID and view ID are present in the URL: +When running locally, `base_id` and `view_id` must be set manually. Navigate to +your view in Airtable. Both IDs are present in the URL: ``` https://airtable.com/appXXXXXXXXXXXXXX/tblXXXXXXXXXXXXXX/viwXXXXXXXXXXXXXX diff --git a/screenly.yml b/screenly.yml index d8f8a42..ac59c22 100644 --- a/screenly.yml +++ b/screenly.yml @@ -63,17 +63,12 @@ settings: help_text: How often to refresh Airtable data. Defaults to 30 seconds. type: number schema_version: 1 - stack_field: - type: string - default_value: '' - title: Stack Field (Kanban) - optional: true - # TODO: help_text type should use a structured oauth:airtable:* type (e.g. oauth:airtable:stack_field) - # to allow the UI to render a proper field picker, similar to base_id using oauth:airtable:base. - help_text: | - For kanban views, the name of the single-select field used to stack columns (e.g. Status). Must be a single-select field. Falls back to the first single-select field if not set. view_id: type: string - title: View ID + title: View optional: false - help_text: The ID of the view to display (e.g. viwXXXXXXXXXXXXXX). Supports grid and kanban views. + help_text: + properties: + help_text: The ID of the view to display. Supports grid and kanban views. + type: oauth:airtable:view + schema_version: 1 diff --git a/screenly_qc.yml b/screenly_qc.yml index c6e6bd0..4d609a2 100644 --- a/screenly_qc.yml +++ b/screenly_qc.yml @@ -63,17 +63,12 @@ settings: help_text: How often to refresh Airtable data. Defaults to 30 seconds. type: number schema_version: 1 - stack_field: - type: string - default_value: '' - title: Stack Field (Kanban) - optional: true - # TODO: help_text type should use a structured oauth:airtable:* type (e.g. oauth:airtable:stack_field) - # to allow the UI to render a proper field picker, similar to base_id using oauth:airtable:base. - help_text: | - For kanban views, the name of the single-select field used to stack columns (e.g. Status). Must be a single-select field. Falls back to the first single-select field if not set. view_id: type: string - title: View ID + title: View optional: false - help_text: The ID of the view to display (e.g. viwXXXXXXXXXXXXXX). Supports grid and kanban views. + help_text: + properties: + help_text: The ID of the view to display. Supports grid and kanban views. + type: oauth:airtable:view + schema_version: 1 diff --git a/src/kanban.test.ts b/src/kanban.test.ts index 7e786a1..1e82a58 100644 --- a/src/kanban.test.ts +++ b/src/kanban.test.ts @@ -114,14 +114,4 @@ describe('renderKanban', () => { ) warn.mockRestore() }) - - it('when stack_field names a non-existent field, should warn and do nothing', () => { - const warn = spyOn(console, 'warn').mockImplementation(() => {}) - renderKanban(RECORDS, FIELDS, 'Missing') - expect(document.querySelectorAll('.kanban-column').length).toBe(0) - expect(warn).toHaveBeenCalledWith( - 'renderKanban: cannot render board: field "Missing" not found or is not a singleSelect', - ) - warn.mockRestore() - }) }) diff --git a/src/kanban.ts b/src/kanban.ts index 5fa64b7..ab00d01 100644 --- a/src/kanban.ts +++ b/src/kanban.ts @@ -115,7 +115,6 @@ function groupByChoice( export function renderKanban( records: AirtableRecord[], fields: AirtableField[], - stackFieldName = '', ): void { const board = document.getElementById('kanban-board') if (!board) { @@ -123,14 +122,11 @@ export function renderKanban( } board.innerHTML = '' - const stackField = stackFieldName - ? fields.find((f) => f.name === stackFieldName && f.type === 'singleSelect') - : fields.find((f) => f.type === 'singleSelect') + const stackField = fields.find((f) => f.type === 'singleSelect') if (!stackField) { - const reason = stackFieldName - ? `field "${stackFieldName}" not found or is not a singleSelect` - : 'no singleSelect field found in schema' - console.warn(`renderKanban: cannot render board: ${reason}`) + console.warn( + 'renderKanban: cannot render board: no singleSelect field found in schema', + ) return } diff --git a/src/main.ts b/src/main.ts index 27d3c9f..aaaec73 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,7 +20,6 @@ document.addEventListener('DOMContentLoaded', async () => { setupTheme() const baseId = getSettingWithDefault('base_id', '') - const stackField = getSettingWithDefault('stack_field', '') const viewId = getSettingWithDefault('view_id', '') const refreshInterval = getSettingWithDefault('refresh_interval', 30) const displayErrors = @@ -28,13 +27,13 @@ document.addEventListener('DOMContentLoaded', async () => { const reportError = createErrorReporter(displayErrors) if (!baseId) { - showError('Please configure the Base ID in settings.') + showError('Please select a Base in settings.') signalReady() return } if (!viewId) { - showError('Please configure the View ID in settings.') + showError('Please select a View in settings.') signalReady() return } @@ -72,7 +71,7 @@ document.addEventListener('DOMContentLoaded', async () => { reportError, async (token) => { const viewData = await fetchViewData(token, baseId, viewId) - renderView(viewData, stackField) + renderView(viewData) }, ) diff --git a/src/render.test.ts b/src/render.test.ts index e99b30d..4bcf915 100644 --- a/src/render.test.ts +++ b/src/render.test.ts @@ -51,24 +51,24 @@ describe('renderView', () => { }) it('should set the table title', () => { - renderView(GRID_VIEW_DATA, '') + renderView(GRID_VIEW_DATA) expect(document.getElementById('table-title')?.textContent).toBe( 'Team Directory', ) }) it('should unhide the table title', () => { - renderView(GRID_VIEW_DATA, '') + renderView(GRID_VIEW_DATA) expect(document.getElementById('table-title')?.hidden).toBe(false) }) it('when viewType is grid, should render table rows', () => { - renderView(GRID_VIEW_DATA, '') + renderView(GRID_VIEW_DATA) expect(document.querySelectorAll('#table-body tr').length).toBe(1) }) it('when viewType is kanban, should render kanban columns', () => { - renderView(KANBAN_VIEW_DATA, '') + renderView(KANBAN_VIEW_DATA) expect(document.querySelectorAll('.kanban-column').length).toBeGreaterThan( 0, ) diff --git a/src/render.ts b/src/render.ts index 49f6813..c8a08c6 100644 --- a/src/render.ts +++ b/src/render.ts @@ -47,7 +47,7 @@ export async function fetchViewData( } } -export function renderView(viewData: ViewData, stackField: string): void { +export function renderView(viewData: ViewData): void { const titleEl = document.getElementById('table-title') if (titleEl) { titleEl.textContent = viewData.tableName @@ -55,7 +55,7 @@ export function renderView(viewData: ViewData, stackField: string): void { } if (viewData.viewType === 'kanban') { - renderKanban(viewData.records, viewData.fields, stackField) + renderKanban(viewData.records, viewData.fields) showView('kanban') } else { const { headers, rows } = recordsToRows(viewData.records, viewData.fields, {