Improve documentation, better typing on media folder selection

This commit is contained in:
Daniel Lautzenheiser 2022-11-02 13:18:06 -04:00
parent 590681d64e
commit fdd51aefa3
20 changed files with 357 additions and 156 deletions

View File

@ -520,8 +520,6 @@ export interface BaseField {
hint?: string;
pattern?: [string, string];
i18n?: boolean | 'translate' | 'duplicate' | 'none';
media_folder?: string;
public_folder?: string;
comment?: string;
}
@ -559,7 +557,7 @@ export interface DateTimeField extends BaseField {
format?: string;
date_format?: boolean | string;
time_format?: boolean | string;
picker_utc?: boolean; // TODO Reimplement
picker_utc?: boolean;
}
export interface FileOrImageField extends BaseField {
@ -567,6 +565,8 @@ export interface FileOrImageField extends BaseField {
default?: string;
media_library?: MediaLibrary;
media_folder?: string;
public_folder?: string;
private?: boolean;
}

View File

@ -5,7 +5,17 @@ import { folderFormatter } from '../formatters';
import { joinUrlPath } from '../urlHelper';
import { basename, isAbsolutePath } from '.';
import type { Config, Field, Collection, CollectionFile, Entry } from '../../interface';
import type {
Config,
Field,
Collection,
CollectionFile,
Entry,
FileOrImageField,
MarkdownField,
ListField,
ObjectField,
} from '../../interface';
export const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES';
@ -14,17 +24,28 @@ function getFileField(collectionFiles: CollectionFile[], slug: string | undefine
return file;
}
function isMediaField(
folderKey: 'media_folder' | 'public_folder',
field: Field | undefined,
): field is FileOrImageField | MarkdownField {
return Boolean(field && folderKey in field);
}
function hasCustomFolder(
folderKey: 'media_folder' | 'public_folder',
collection: Collection | undefined | null,
slug: string | undefined,
field: Field | undefined,
) {
): field is FileOrImageField | MarkdownField {
if (!collection) {
return false;
}
if (field && field[folderKey]) {
if (!isMediaField(folderKey, field)) {
return false;
}
if (field[folderKey]) {
return true;
}
@ -47,7 +68,7 @@ function evaluateFolder(
config: Config,
c: Collection,
entryMap: Entry | undefined,
field: Field | undefined,
field: FileOrImageField | MarkdownField,
) {
let currentFolder = config[folderKey]!;
@ -140,12 +161,17 @@ function traverseFields(
config: Config,
collection: Collection,
entryMap: Entry | undefined,
field: Field,
field: FileOrImageField | MarkdownField | ListField | ObjectField,
fields: Field[],
currentFolder: string,
): string | null {
const matchedField = fields.filter(f => f === field)[0];
if (matchedField) {
const matchedField = fields.filter(f => f === field)[0] as
| FileOrImageField
| MarkdownField
| ListField
| ObjectField
| undefined;
if (matchedField && isMediaField(folderKey, matchedField)) {
return folderFormatter(
matchedField[folderKey] ? matchedField[folderKey]! : `{{${folderKey}}}`,
entryMap,
@ -157,13 +183,13 @@ function traverseFields(
}
for (const f of fields) {
const field = { ...f };
if (!field[folderKey]) {
const childField: Field = { ...f };
if (isMediaField(folderKey, childField) && !childField[folderKey]) {
// add identity template if doesn't exist
field[folderKey] = `{{${folderKey}}}`;
childField[folderKey] = `{{${folderKey}}}`;
}
const folder = folderFormatter(
field[folderKey]!,
isMediaField(folderKey, childField) ? childField[folderKey] ?? '' : '',
entryMap,
collection,
currentFolder,
@ -171,24 +197,24 @@ function traverseFields(
config.slug,
);
let fieldFolder = null;
if ('fields' in field && field.fields) {
if ('fields' in childField && childField.fields) {
fieldFolder = traverseFields(
folderKey,
config,
collection,
entryMap,
field,
field.fields,
childField,
childField.fields,
folder,
);
} else if ('types' in field && field.types) {
} else if ('types' in childField && childField.types) {
fieldFolder = traverseFields(
folderKey,
config,
collection,
entryMap,
field,
field.types,
childField,
childField.types,
folder,
);
}
@ -209,9 +235,7 @@ export function selectMediaFolder(
const name = 'media_folder';
let mediaFolder = config[name];
const customFolder = hasCustomFolder(name, collection, entryMap?.slug, field);
if (customFolder) {
if (hasCustomFolder(name, collection, entryMap?.slug, field)) {
const folder = evaluateFolder(name, config, collection!, entryMap, field);
if (folder.startsWith('/')) {
// return absolute paths as is

View File

@ -64,12 +64,9 @@ const DateTimeControl = ({
const format = field.format;
// dateFormat and timeFormat are strictly for modifying input field with the date/time pickers
const dateFormat: string | boolean = field.date_format ?? false;
const dateFormat: string | boolean = field.date_format ?? true;
// show time-picker? false hides it, true shows it using default format
let timeFormat: string | boolean = field.time_format ?? false;
if (typeof timeFormat === 'undefined') {
timeFormat = true;
}
const timeFormat: string | boolean = field.time_format ?? true;
return {
format,

View File

@ -3,9 +3,10 @@ group: Accounts
title: Azure
weight: 20
---
For repositories stored on Azure, the `azure` backend allows CMS users to log in directly with their Azure account. Note that all users must have write access to your content repository for this to work.
## Authentication
In order to get Static CMS working with Azure DevOps, you need a Tenant Id and an Application Id.
1. If you do not have an Azure account, [create one here](https://azure.microsoft.com/en-us/free/?WT.mc_id=A261C142F) and make sure to have a credit card linked to the account.

View File

@ -5,7 +5,9 @@ weight: 20
---
For repositories stored on Bitbucket, the `bitbucket` backend allows CMS users to log in directly with their Bitbucket account. Note that all users must have write access to your content repository for this to work.
To enable it:
## Authentication
To enable Bitbucket authentication it:
1. Follow the authentication provider setup steps in the [Netlify docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider).
2. Add the following lines to your Static CMS `config.yml` file:

View File

@ -5,6 +5,8 @@ weight: 30
---
For repositories stored on GitHub, the `github` backend allows CMS users to log in directly with their GitHub account. Note that all users must have push access to your content repository for this to work.
## Authentication
Because Github requires a server for authentication, Netlify facilitates basic GitHub authentication.
To enable basic GitHub authentication:

View File

@ -7,7 +7,7 @@ For repositories stored on GitLab, the `gitlab` backend allows CMS users to log
**Note:** GitLab default branch is protected by default, thus typically requires `maintainer` permissions in order for users to have push access.
## Authorization
## Authentication
With GitLab's PKCE authorization, users can authenticate with GitLab directly from the client. To do this:

View File

@ -6,12 +6,19 @@ weight: 10
The boolean widget translates a toggle switch input to a true/false value.
- **Name:** `boolean`
- **UI:** toggle switch
- **Data type:** boolean
- **Options:**
- `default`: accepts `true` or `false`; defaults to `false` when `required` is set to `false`
- **Example:**
```yaml
- {label: "Draft", title: "draft", widget: "boolean", default: true}
```
## Widget options
For common options, see [Common widget options](/docs/widgets#common-widget-options).
|Name|Type|Default|Description|
|----|----|-------|-----------|
|default|boolean|`false`|_Optional_. The default value for the field|
## Example
```yaml
name: draft
label: Draft
widget: boolean
default: true
```

View File

@ -6,18 +6,21 @@ weight: 11
The code widget provides a code editor (powered by [Codemirror](https://codemirror.net)) with optional syntax awareness. Can output the raw code value or an object with the selected language and the raw code value.
- **Name:** `code`
- **UI:** code editor
- **Data type:** string
- **Options:**
- `default_language`: optional; default language to use
- `allow_language_selection`: optional; defaults to `false`: allows syntax to be changed
- `keys`: optional; sets key names for code and lang if outputting an object; defaults to `{ code: 'code', lang: 'lang' }`
- `output_code_only`: set to `true` to output the string value only, defaults to `false`
## Widget options
- **Example:**
```yaml
- label: 'Code'
title: 'code'
widget: 'code'
```
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| ------------------------ | ------- | -------------------------------- | -------------------------------------------------------------------- |
| default_language | string | | _Optional_. Default language to use |
| allow_language_selection | boolean | `false` | _Optional_. Allows language syntax to be changed |
| keys | boolean | `{ code: 'code', lang: 'lang' }` | _Optional_. Sets key names for code and lang if outputting an object |
| output_code_only | string | `true` | _Optional_. Set to `true` to output the string value only |
## Example
```yaml
name: code
label: Code
widget: code
```

View File

@ -6,18 +6,31 @@ weight: 12
The color widget translates a color picker to a color string.
- **Name:** `color`
- **UI:** color picker
- **Data type:** string
- **Options:**
- `default`: accepts a string; defaults to an empty string. Sets the default value
- `allow_input`: accepts a boolean, defaults to `false`. Allows manual editing of the color input value
- `enable_alpha`: accepts a boolean, defaults to `false`. Enables Alpha editing
- **Example:**
```yaml
- { label: 'Color', title: 'color', widget: 'color' }
```
- **Example:**
```yaml
- { label: 'Color', title: 'color', widget: 'color', enable_alpha: true, allow_input: true }
```
## Widget options
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| ------------ | ------- | ------- | ---------------------------------------------------------- |
| default | string | `''` | _Optional_. The default value for the field |
| allow_input | boolean | `false` | _Optional_. Allows manual editing of the color input value |
| enable_alpha | boolean | `false` | _Optional_. Enables Alpha editing |
## Examples
### Basic
```yaml
name: color
label: Color
widget: color
```
### Kitchen Sink
```yaml
name: color
label: Color
widget: color
enable_alpha: true
allow_input: true
```

View File

@ -6,23 +6,49 @@ weight: 13
The datetime widget translates a datetime picker to a datetime string.
- **Name:** `datetime`
- **UI:** datetime picker
- **Data type:** Moment.js-formatted datetime string
- **Options:**
- `default`: accepts a datetime string, or an empty string to accept blank input; otherwise defaults to current datetime
- `format`: sets storage format; accepts Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/); defaults to raw Date object (if supported by output format)
- `date_format`: sets date display format in UI; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format.
- `time_format`: sets time display format in UI; boolean or Moment.js [tokens](https://momentjs.com/docs/#/parsing/string-format/). If `true` use default locale format, `false` hides time-picker.
- `picker_utc`: _(default: `false`)_ when set to `true`, the datetime picker will display times in UTC. When `false`, the datetime picker will display times in the user's local timezone. When using date-only formats, it can be helpful to set this to `true` so users in all timezones will see the same date in the datetime picker.
- **Example:**
```yaml
- label: "Start time"
title: "start"
widget: "datetime"
default: ""
date_format: "DD.MM.YYYY" # e.g. 24.12.2021
time_format: "HH:mm" # e.g. 21:07
format: "LLL"
picker_utc: false
```
## Widget options
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| ----------- | ---------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| default | string | `Current Date and Time` | _Optional_. The default value for the field. Accepts a datetime string, or an empty string to accept blank input. |
| format | string | `yyyy-MM-dd'T'HH:mm:ss.SSSXXX` | _Optional_. Sets storage format. Accepts [date-fns tokens](https://date-fns.org/v2.29.3/docs/format) |
| date_format | string<br />\| boolean | `true` | _Optional_. Sets date display format in UI.<ul><li>`string` - Accepts [date-fns tokens](https://date-fns.org/v2.29.3/docs/format)</li><li>`true` - Uses default locale format</li><li>`false` - If `time_format` is `true` or a string, then date picker is hidden</li></ul> |
| time_format | string<br />\| boolean | `true` | _Optional_. Sets time display format in UI.<ul><li>`string` - Accepts [date-fns tokens](https://date-fns.org/v2.29.3/docs/format)</li><li>`true` - Uses default locale format</li><li>`false` - Hides the time picker</li></ul> |
| picker_utc | boolean | `false` | _Optional_. <ul><li>`true` - The datetime picker will display times in UTC</li><li>`false` - The datetime picker will display times in the user's local timezone</li></ul> When using date-only formats, it can be helpful to set this to `true` so users in all timezones will see the same date in the datetime picker |
## Examples
### Date Time Picker
```yaml
name: 'datetime'
label: 'Datetime'
widget: 'datetime'
date_format: 'dd.MM.yyyy' # e.g. 24.12.2022
time_format: 'HH:mm' # e.g. 21:07
format: 'yyyy-MM-dd HH:mm' # e.g. 2022-12-24 21:07
```
### Date Picker
```yaml
name: 'date'
label: 'Date'
widget: 'datetime'
date_format: 'dd.MM.yyyy' # e.g. 24.12.2022
time_format: false
format: 'yyyy-MM-dd' # e.g. 2022-12-24
```
### Time Picker
```yaml
name: 'date'
label: 'Date'
widget: 'datetime'
date_format: false
time_format: 'HH:mm' # e.g. 21:07
format: 'HH:mm' # e.g. 21:07
```

View File

@ -4,23 +4,34 @@ title: Overview
weight: 0
---
Widgets define the data type and interface for entry fields. Static CMS comes with several built-in widgets. Click the widget names in the sidebar to jump to specific widget details. We're always adding new widgets, and you can also [create your own](/docs/custom-widgets)!
Widgets define the data type and interface for entry fields. Static CMS comes with several built-in widgets. Click the widget names in the sidebar to jump to specific widget details. You can also [create your own](/docs/custom-widgets)!
Widgets are specified as collection fields in the Static CMS `config.yml` file. Note that [YAML syntax](https://en.wikipedia.org/wiki/YAML#Basic_components) allows lists and objects to be written in block or inline style, and the code samples below include a mix of both.
To see working examples of all of the built-in widgets, try making a 'Kitchen Sink' collection item on the [CMS demo site](https://cms-demo.netlify.com). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/dev-test/config.yml) to see how each field was configured.
To see working examples of all of the built-in widgets, try making a 'Kitchen Sink' collection item on the [CMS demo site](https://static-cms-demo.netlify.com). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/dev-test/config.yml) to see how each field was configured.
## Common widget options
The following options are available on all fields:
- `required`: specify as `false` to make a field optional; defaults to `true`
- `hint`: optionally add helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links.
- `pattern`: add field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation)
- **Example:**
```yaml
label: "Title"
title: "title"
widget: "string"
pattern: ['.{12,}', "Must have at least 12 characters"]
```
| Name | Type | Default | Description |
| ------------- | ----------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | | The name of the field |
| widget | string | `'string'` | _Optional_. The type of widget to render for the field |
| label | string | `name` | _Optional_. The display name of the field |
| required | boolean | `true` | _Optional_. Specify as `false` to make a field optional |
| hint | string | | _Optional_. Adds helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links. |
| pattern | string | | _Optional_. Adds field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) |
| i18n | boolean<br />\|'translate'<br />\|'duplicate'<br />\|'none' | | _Optional_. <img src="https://img.shields.io/badge/-Beta%20Feature-blue" alt="Beta Feature" /><ul><li>`translate` - Allows translation of the field</li><li>`duplicate` - Duplicates the value from the default locale</li><li>`true` - Accept parent values as default</li><li>`none` or `false` - Exclude field from translations</li></ul> |
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) |
### Example
```yaml
name: title
label: Title
widget: string
pattern: ['.{12,}', 'Must have at least 12 characters']
```

View File

@ -155,6 +155,19 @@ const DocsContent = styled('div')(
}
}
& table thead tr th,
& table thead tr td {
white-space: nowrap;
}
& table tbody tr td {
white-space: nowrap;
}
& table tbody tr td:last-child {
white-space: normal;
}
& pre {
display: block;
line-height: 1.25rem;
@ -197,8 +210,9 @@ const DocsContent = styled('div')(
padding: 0.2em 0.4em;
margin: 0;
border-radius: 3px;
color: ${theme.palette.text.primary};
background-color: ${
theme.palette.mode === 'light' ? 'rgba(175,184,193,0.2)' : 'rgba(110,118,129,0.4)'
theme.palette.mode === 'light' ? 'rgba(175,184,193,0.2)' : 'rgba(110,118,129,0.75)'
};
}
@ -222,59 +236,6 @@ const DocsContent = styled('div')(
z-index: -1;
}
& table {
width: 100%;
max-width: 100%;
border-spacing: 0;
margin: 24px 0;
border: 1px solid ${theme.palette.text.secondary};
border-radius: 4px;
overflow: hidden;
}
&.editor table {
border: none;
border-radius: 0;
overflow: visible;
}
& table thead {
background: ${theme.palette.background.paper};
}
& table th,
& table thead td {
font-weight: 700;
height: 56px;
box-sizing: border-box;
text-align: left;
}
& table th,
& table thead td,
& table td {
padding: 8px 16px;
}
& table tr:not(:first-of-type) th,
& table tr:not(:first-of-type) thead td,
& table tbody tr td {
border-top: 1px solid ${theme.palette.text.secondary};
}
& table tr td {
background-color: ${theme.palette.background.paper};
}
& table tbody tr td {
height: 52px;
box-sizing: border-box;
}
& table tbody tr:hover td {
background-color: ${theme.palette.background.paper};
}
& ol,
& ul {
padding: 0 0 0 1.5rem;

View File

@ -0,0 +1,34 @@
import { styled } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableContainer from '@mui/material/TableContainer';
import type { ReactNode } from 'react';
const StyledTableContainer = styled(TableContainer)(
({ theme }) => `
& td {
color: ${theme.palette.text.secondary};
}
& td:nth-of-type(2) {
color:
${theme.palette.mode === 'light' ? '#751365' : '#ffb6ec'};
}
`,
);
interface DocsTableProps {
children?: ReactNode | ReactNode[];
}
const DocsTable = ({ children = [] }: DocsTableProps) => {
return (
<StyledTableContainer>
<Table sx={{ width: '100%' }} aria-label="doc table">
{children}
</Table>
</StyledTableContainer>
);
};
export default DocsTable;

View File

@ -0,0 +1,13 @@
import MuiTableBody from '@mui/material/TableBody';
import type { ReactNode } from 'react';
interface TableBodyProps {
children?: ReactNode;
}
const TableBody = ({ children }: TableBodyProps) => {
return <MuiTableBody>{children}</MuiTableBody>;
};
export default TableBody;

View File

@ -0,0 +1,37 @@
import TableCell from '@mui/material/TableCell';
import Typography from '@mui/material/Typography';
import type { ReactNode } from 'react';
interface TableBodyCellProps {
children?: ReactNode;
}
const TableBodyCell = ({ children }: TableBodyCellProps) => {
return (
<TableCell
scope="row"
sx={{
padding: '16px 12px',
'&:first-child, &:first-child': {
paddingLeft: 0,
},
'&:last-child, &:last-child': {
paddingRight: 0,
},
}}
>
<Typography
component="span"
sx={{
fontSize: '13px',
fontFamily: 'Consolas, Menlo, Monaco, Andale Mono, Ubuntu Mono, monospace',
}}
>
{children}
</Typography>
</TableCell>
);
};
export default TableBodyCell;

View File

@ -0,0 +1,13 @@
import MuiTableHead from '@mui/material/TableHead';
import type { ReactNode } from 'react';
interface TableHeadProps {
children?: ReactNode;
}
const TableHead = ({ children }: TableHeadProps) => {
return <MuiTableHead>{children}</MuiTableHead>;
};
export default TableHead;

View File

@ -0,0 +1,28 @@
import TableCell from '@mui/material/TableCell';
import type { ReactNode } from 'react';
interface TableHeaderCellProps {
children?: ReactNode;
}
const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
return (
<TableCell
sx={{
fontWeight: 600,
padding: '16px 12px',
'&:first-child, &:first-child': {
paddingLeft: 0,
},
'&:last-child, &:last-child': {
paddingRight: 0,
},
}}
>
{children}
</TableCell>
);
};
export default TableHeaderCell;

View File

@ -0,0 +1,15 @@
import MuiTableRow from '@mui/material/TableRow';
import type { ReactNode } from 'react';
interface TableRowProps {
children?: ReactNode;
}
const TableRow = ({ children }: TableRowProps) => {
return (
<MuiTableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>{children}</MuiTableRow>
);
};
export default TableRow;

View File

@ -7,6 +7,11 @@ import remarkGfm from 'remark-gfm';
import Blockquote from '../../components/docs/components/Blockquote';
import Header2 from '../../components/docs/components/Header2';
import Header3 from '../../components/docs/components/Header3';
import DocsTable from '../../components/docs/components/table/Table';
import TableBody from '../../components/docs/components/table/TableBody';
import TableBodyCell from '../../components/docs/components/table/TableBodyCell';
import TableHead from '../../components/docs/components/table/TableHead';
import TableHeaderCell from '../../components/docs/components/table/TableHeaderCell';
import DocsContent from '../../components/docs/DocsContent';
import DocsLeftNav from '../../components/docs/DocsLeftNav';
import DocsRightNav from '../../components/docs/DocsRightNav';
@ -87,7 +92,16 @@ const Docs = ({ docsGroups, title, slug, description = '', source }: DocsProps)
<Typography variant="h1">{title}</Typography>
<MDXRemote
{...source}
components={{ h2: Header2, h3: Header3, blockquote: Blockquote }}
components={{
h2: Header2,
h3: Header3,
blockquote: Blockquote,
table: DocsTable,
thead: TableHead,
tbody: TableBody,
th: TableHeaderCell,
td: TableBodyCell,
}}
/>
</DocsContent>
</StyledDocsContentWrapper>