diff --git a/.editorconfig b/.editorconfig index abfebe906f..32b4d70cbf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ insert_final_newline = true indent_style = tab [*.js] -indent_size = 4 +indent_size = 2 [*.json] indent_style = space diff --git a/.eslintignore b/.eslintignore index 9bef6ff66d..b56f42833b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,4 @@ docs/* fields/types/**/lib/* node_modules/* test/* +website/* diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index de49483053..a636cc24ee 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,10 +3,9 @@ Please ask questions and support requests on: * https://stackoverflow.com/questions/tagged/keystonejs - * https://gitter.im/keystonejs/keystone - New features can be requested and voted upon on: - * https://productpains.com/product/keystonejs + Join the KeystoneJS Slack for discussion with the community & contributors: + * https://launchpass.com/keystonejs --> ### Expected behavior @@ -23,7 +22,7 @@ ### Steps to reproduce the actual/current behavior - + @@ -33,4 +32,5 @@ | Software | Version | ---------------- | ------- | Keystone | -| Node | +| Node.js | +| Browser | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0fefdd3397..e9020a1986 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,29 +1,30 @@ ## Description of changes - ## Related issues (if any) ## Testing -- [ ] Please confirm `npm run test-all` ran successfully. + - [ ] List browser version(s) any admin UI changes were tested in: + - [ ] Please confirm you've added (or verified) test coverage for this change. + - [ ] Please confirm `npm run test-all` ran successfully. diff --git a/.gitignore b/.gitignore index 618b8d2a41..573ee573bc 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,9 @@ reports test/e2e/drivers/* +package-lock.json yarn.lock + +# Website dependencies +website/.cache +website/public diff --git a/.travis.yml b/.travis.yml index 60d334fe54..f2b49e5d1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,21 +44,7 @@ matrix: - npm run test-admin env: - JOB=unit_tests_node_6 - - node_js: '5' - script: - - npm run test-admin - env: - - JOB=unit_tests_node_5 - - node_js: '4' - script: - - npm run test-admin - env: - - JOB=unit_tests_node_4 - - node_js: '0.12' - script: - - npm run test-admin - env: - - JOB=unit_tests_node_0.12 + before_script: - sleep 15 @@ -76,8 +62,9 @@ services: git: depth: 10 cache: + yarn: true directories: - - node_modules + - node_modules addons: apt: sources: diff --git a/HISTORY.md b/HISTORY.md index 13d3dd9fdb..6181334b72 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,9 @@ KeystoneJS is maintained by [@JedWatson](https://github.com/JedWatson) and an amazing team of contributors. All contributions are given credit here except for Jed's. +Release notes for Keystone 4 beta & release candidates can be found on the [GitHub releases]( https://github.com/keystonejs/keystone/releases) page. + +Changes for production releases are included below. ## v0.3.22 / 2016-07-22 diff --git a/README.md b/README.md index 7a9c417d3f..331a53f481 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,51 @@ -![KeystoneJS](http://keystonejs.com/images/logo.svg) +![KeystoneJS](http://v3.keystonejs.com/images/logo.svg) =================================== [![Build Status](https://travis-ci.org/keystonejs/keystone.svg?branch=master)](https://travis-ci.org/keystonejs/keystone) -[KeystoneJS](http://keystonejs.com) is a powerful Node.js content management system and web app framework built on [express](http://expressjs.com) and [mongoose](http://mongoosejs.com). Keystone makes it easy to create sophisticated web sites and apps, and comes with a beautiful auto-generated Admin UI. + - [About Keystone](#about) + - [Getting Started](#getting-started) + - [Community](#community) + - [Contributing](#contributing) + - [License](#license) -Check out [keystonejs.com](http://keystonejs.com) for documentation and guides. +## About Keystone -You can also deploy a starter project to [Heroku](https://www.heroku.com/) for free to try it out: +[KeystoneJS](http://keystonejs.com) is a powerful Node.js content management system and web app framework built on the [Express](https://expressjs.com/) web framework and [Mongoose ODM](http://mongoosejs.com). Keystone makes it easy to create sophisticated web sites and apps, and comes with a beautiful auto-generated Admin UI. -[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/JedWatson/keystone-starter) +### Documentation +For Keystone v4 documentation and guides, see [keystonejs.com](https://keystonejs.com). -## Keystone 4.0 Beta Released!!! +For Keystone v0.3 documentation, see [v3.keystonejs.com](https://v3.keystonejs.com). -We've been working on a major update to KeystoneJS for the last year, and it's a complete rebuild of Keystone's Admin UI and internal architecture. Improvements include: +### Keystone 4.0 Release Candidate (RC) -* The Admin UI has been re-written as a single page app using React.js, Redux and Elemental UI +We've been working on a major update to KeystoneJS. Keystone 4 is a complete rebuild of Keystone's Admin UI and internal architecture. + +Improvements include: + +* The Admin UI has been re-written as a single page app using [React.js](https://reactjs.org), [Redux](https://redux.js.org/), and [Elemental UI](http://elemental-ui.com/) * An updated API for Lists and Fields * Better support for using Keystone without Express, or with your own express instance * Core functionality has been refactored and we're breaking Keystone up into separate npm packages * Startup time has been significantly reduced -* LocalFile, S3File and AzureFile have been replaced by a new generic `keystone.Storage` engine and File field +* LocalFile, S3File, and AzureFile have been replaced by a new generic `keystone.Storage` engine and File field * We have much higher unit and end-to-end test coverage -Please try out the beta and let us know what you think: +Please try out Keystone 4 and let us know what you think: ``` -npm install --save keystone@next +npm install --save keystone ``` -We'll be publishing a summary of the new features, changes and improvements as we get closer to the final release. In the meantime, see the [v0.3 -> v4.0 Upgrade Guide](https://github.com/keystonejs/keystone/blob/master/docs/guides/v0.3-to-v4.0-Upgrade-Guide.md) for information on what's changed. - -Also check out our [demo site](http://demo.keystonejs.com/), which has been updated to the new version! - - -## About - -Keystone gives you: -* A simple way to create a dynamic web site or app with well-structured routes, templates and models -* A beautiful Admin UI based on the database models you define -* Enhanced `models` with additional field types and functionality, building on those natively supported by Mongoose -* Out of the box session management and authentication -* An updates framework for managing data updates or initialisation -* Integration with Cloudinary for image uploading, storage and resizing -* Integration with Mandrill for sending emails easily -* Integration with Google Places for clever location fields -* Integration with Embedly for powerful video and rich media embedding tools - -... plus a lot of other tools and utilities to make creating complex web apps easier. - -Use our [Yeoman Generator](https://github.com/keystonejs/generator-keystone) to get up and running with KeystoneJS quickly, then check out our getting started guide & docs at [keystonejs.com/docs/getting-started](http://keystonejs.com/docs/getting-started). - -We have a demo website at [demo.keystonejs.com](http://demo.keystonejs.com/) where you can play with the Keystone Admin UI, and you can [read the source](https://github.com/keystonejs/keystone-demo) to see how it was built. - -### Community - -We have a friendly, growing community and welcome everyone to get involved. +We'll be publishing a summary of the new features, changes, and improvements as we get closer to the final release. In the meantime, see the [v0.3 -> v4.0 Upgrade Guide](https://keystonejs.com/guides/v-0-3-to-v-4-0-upgrade-guide) for information on what's changed. -Here are some ways: +Also check out our [demo site](http://demo.keystonejs.com), which has been updated to the new version! -* Follow [@KeystoneJS](https://twitter.com/KeystoneJS) on twitter for news and announcements -* Vote on the next features on [ProductPains](https://productpains.com/product/keystonejs) -* Chat with us [![Join the chat at https://gitter.im/keystonejs/keystone](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/keystonejs/keystone?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -* If you've got ideas, questions or need some advice, check out the [KeystoneJS Google Group](https://groups.google.com/d/forum/keystonejs) -* Ask technical questions on [Stack Overflow](http://stackoverflow.com/questions/tagged/keystone.js) and tag them `keystonejs` -* Report bugs and issues on our [issue tracker](https://github.com/keystonejs/keystone/issues) -* ... or preferably, submit pull request with patches and / or new features +## Getting Started -We love to hear feedback about Keystone and the projects you're using it for. Ping us at [@KeystoneJS](https://twitter.com/KeystoneJS) on twitter. - -#### Related Projects -If you are using KeystoneJS in any projects we encourage you to add it to our [Related Projects Page](https://github.com/keystonejs/keystone/wiki/Related-Projects). This is also the place to find generators and such that bundle KeystoneJS. - -### Contributing - -If you can, please contribute by reporting issues, discussing ideas, or submitting pull requests with patches and new features. We do our best to respond to all issues and pull requests within a day or two, and make patch releases to npm regularly. - -If you're going to contribute code, please follow our [coding standards](https://github.com/keystonejs/keystone/wiki/Coding-Standards) and read our [CONTRIBUTING.md](https://github.com/keystonejs/keystone/blob/master/CONTRIBUTING.md). - -## Usage - -**Check out the [KeystoneJS Getting Started Guide](http://keystonejs.com/getting-started) to start using KeystoneJS.** +This section provides a short intro to Keystone. Check out the [Getting Started Guide](https://keystonejs.com/getting-started) in the Keystone documentation for a more comprehensive introduction. ### Installation @@ -94,86 +58,63 @@ $ yo keystone Answer the questions, and the generator will create a new project based on the options you select, and install the required packages from **npm**. -Alternatively, to include Keystone in an existing project or start from scratch (without Yeoman), specify `keystone: "^0.3.9"` in the `dependencies` array of your `package.json` file, and run `npm install` from your terminal. +Alternatively, to include Keystone in an existing project or start from scratch (without Yeoman), specify `keystone: "4.0.0"` in the `dependencies` array of your `package.json` file, and run `npm install` from your terminal. -Then read through the [Documentation](http://keystonejs.com/docs) and the [Example Projects](http://keystonejs.com/examples) to understand how to use it. +Then read through the [Documentation](https://keystonejs.com/documentation) and the [Example Projects](http://v3.keystonejs.com/examples) to understand how to use it. ### Configuration -Config variables can be passed in an object to the `keystone.init` method, or can be set any time before `keystone.start` is called using `keystone.set(key, value)`. This allows for a more flexible order of execution (e.g. if you refer to Lists in your routes, you can set the routes after configuring your Lists, as in the example above). +Config variables can be passed in an object to the `keystone.init` method, or can be set any time before `keystone.start` is called using `keystone.set(key, value)`. This allows for a more flexible order of execution. For example, if you refer to Lists in your routes you can set the routes after configuring your Lists. -See the [KeystoneJS configuration documentation](http://keystonejs.com/docs/configuration) for details and examples of the available configuration options. +See the [KeystoneJS configuration documentation](https://keystonejs.com/documentation/configuration) for details and examples of the available options. ### Database field types -Keystone builds on the basic data types provided by mongo and allows you to easily add rich, functional fields to your application's models. +Keystone builds on the basic data types provided by MongoDB and allows you to easily add rich, functional fields to your application's models. You get helper methods on your models for dealing with each field type easily (such as formatting a date or number, resizing an image, getting an array of the available options for a select field, or using Google's Places API to improve addresses) as well as a beautiful, responsive admin UI to edit your data with. -See the [KeystoneJS database documentation](http://keystonejs.com/docs/database) for details and examples of the various field types, as well as how to set up and use database models in your application. - -Keystone's field types include: - -* [Boolean](http://keystonejs.com/docs/database/#fieldtypes-boolean) -* [Color](http://keystonejs.com/docs/database/#fieldtypes-color) -* [Date](http://keystonejs.com/docs/database/#fieldtypes-date) -* [Datetime](http://keystonejs.com/docs/database/#fieldtypes-datetime) -* [Email](http://keystonejs.com/docs/database/#fieldtypes-email) -* [Html](http://keystonejs.com/docs/database/#fieldtypes-html) -* [Key](http://keystonejs.com/docs/database/#fieldtypes-key) -* [Location](http://keystonejs.com/docs/database/#fieldtypes-location) -* [Markdown](http://keystonejs.com/docs/database/#fieldtypes-markdown) -* [Money](http://keystonejs.com/docs/database/#fieldtypes-money) -* [Name](http://keystonejs.com/docs/database/#fieldtypes-name) -* [Number](http://keystonejs.com/docs/database/#fieldtypes-number) -* [Password](http://keystonejs.com/docs/database/#fieldtypes-password) -* [Select](http://keystonejs.com/docs/database/#fieldtypes-select) -* [Text](http://keystonejs.com/docs/database/#fieldtypes-text) -* [Textarea](http://keystonejs.com/docs/database/#fieldtypes-textarea) -* [Url](http://keystonejs.com/docs/database/#fieldtypes-url) -* [Azure File](http://keystonejs.com/docs/database/#fieldtypes-azurefile) -* [CloudinaryImage](http://keystonejs.com/docs/database/#fieldtypes-cloudinaryimage) -* [CloudinaryImages](http://keystonejs.com/docs/database/#fieldtypes-cloudinaryimages) -* [Embedly](http://keystonejs.com/docs/database/#fieldtypes-embedly) -* [LocalFile](http://keystonejs.com/docs/database/#fieldtypes-localfile) -* [S3 File](http://keystonejs.com/docs/database/#fieldtypes-s3file) - -Keystone also has [Relationship fields](http://keystonejs.com/docs/database#relationships) for managing one-to-many and many-to-many relationships between different models. +See the [KeystoneJS database documentation](https://keystonejs.com/documentation/database) for details and examples of the various field types, as well as how to set up and use database models in your application. ### Running KeystoneJS in Production When you deploy your KeystoneJS app to production, be sure to set your `ENV` environment variable to `production`. + You can do this by setting `NODE_ENV=production` in your `.env` file, which gets handled by [dotenv](https://github.com/motdotla/dotenv). -Setting your environment enables certain features, including template caching, simpler error reporting and html minification, that are important in production but annoying in development. +Setting your environment enables certain features (including template caching, simpler error reporting, and HTML minification) that are important in production but annoying in development. -### Linking Keystone for Development and Testing +## Community -If you want to test or develop against the `master` branch of KeystoneJS (or against your own branch), rather than a published version on **npm**, you just need to check it out then use `npm link` to link it to your project. On Mac OS, this is done like this: +We have a friendly, growing community and welcome everyone to get involved: -* Clone KeystoneJS locally, e.g. to `~/Development/KeystoneJS` -* From the KeystoneJS directory, run `sudo npm link` (you will need to enter your system password) -* From your project directory, e.g. `~/Development/MySite` (the one with your `package.json` file in it) run `npm link keystone`. This will create a link between `~/Development/MySite/node_modules/keystone` and `~/Development/KeystoneJS`. +* Follow [@KeystoneJS](https://twitter.com/KeystoneJS) on twitter for news and announcements. +* Ask technical questions on [Stack Overflow](http://stackoverflow.com/questions/tagged/keystone.js) and tag them `keystonejs.` +* Report bugs and feature suggestions on our GitHub [issue tracker](https://github.com/keystonejs/keystone/issues). +* Join the [KeystoneJS Slack](https://launchpass.com/keystonejs) for general discussion with the Keystone community and contributors. -Then `require('keystone')` normally in your app - the development copy will be used. Note that running `npm update` will ignore new versions of keystone that have been published. +We love to hear feedback about Keystone and the projects you're using it for. Ping us at [@KeystoneJS](https://twitter.com/KeystoneJS) on Twitter. + +### Contributing -To go back to using a published version of KeystoneJS from npm, from your project directory, run `npm unlink keystone` then `npm install`. +If you can, please contribute by reporting issues, discussing ideas, helping answer questions from other developers, or submitting pull requests with patches and new features. We do our best to respond to all issues and pull requests, and make patch releases to npm regularly. -#### Testing -To run the test suite run `npm test`. +If you're going to contribute code, please follow our [coding standards](https://github.com/keystonejs/keystone/wiki/Coding-Standards) and read our [Contributing Guide](https://github.com/keystonejs/keystone/blob/master/CONTRIBUTING.md). -## Thanks +### Related Projects +If you are using KeystoneJS in any projects we encourage you to add to our [Related Projects Page](https://github.com/keystonejs/keystone/wiki/Related-Projects). This is also the place to find generators and other projects that bundle KeystoneJS. -KeystoneJS is a free and open source community-driven project. Thanks to our many [contributors](https://github.com/keystonejs/keystone/graphs/contributors) and [users](https://github.com/keystonejs/keystone/stargazers) for making it great. +### Thanks -Keystone's development is led by [Jed Watson](https://github.com/JedWatson), [Joss Mackison](https://github.com/jossmac) and [Max Stoiber](https://github.com/mxstbr) and supported by [Thinkmill](http://thinkmill.com.au) in Sydney, Australia. +KeystoneJS is a free and open source community-driven project. Thanks to our many [contributors](https://github.com/keystonejs/keystone/graphs/contributors) and [users](https://github.com/keystonejs/keystone/stargazers) for making it great. +Keystone's development has been led by key contributors including [Jed Watson](https://github.com/JedWatson), [Joss Mackison](https://github.com/jossmac), and [Max Stoiber](https://github.com/mxstbr) and is proudly supported by [Thinkmill](https://thinkmill.com.au) in Sydney, Australia. ## License (The MIT License) -Copyright (c) 2016 Jed Watson +Copyright (c) 2016-2018 Jed Watson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/admin/client/App/components/Navigation/Primary/index.js b/admin/client/App/components/Navigation/Primary/index.js index 4be38b24c3..a0f003ab4f 100644 --- a/admin/client/App/components/Navigation/Primary/index.js +++ b/admin/client/App/components/Navigation/Primary/index.js @@ -91,7 +91,8 @@ var PrimaryNavigation = React.createClass({ return this.props.sections.map((section) => { // Get the link and the class name - const href = section.lists[0].external ? section.lists[0].path : `${Keystone.adminPath}/${section.lists[0].path}`; + const to = !section.lists[0].external && `${Keystone.adminPath}/${section.lists[0].path}`; + const href = section.lists[0].external && section.lists[0].path; const isActive = this.props.currentSectionKey && this.props.currentSectionKey === section.key; const className = isActive ? 'primary-navbar__item--active' : null; @@ -101,7 +102,8 @@ var PrimaryNavigation = React.createClass({ key={section.key} label={section.label} className={className} - to={href} + to={to} + href={href} > {section.label} diff --git a/admin/client/App/elemental/Button/index.js b/admin/client/App/elemental/Button/index.js index e0a10b5045..b105b83131 100644 --- a/admin/client/App/elemental/Button/index.js +++ b/admin/client/App/elemental/Button/index.js @@ -23,7 +23,7 @@ class Button extends Component { render () { var { active, - aphroditeStyles, + cssStyles, block, className, color, @@ -43,7 +43,7 @@ class Button extends Component { block ? commonClasses.block : null, disabled ? commonClasses.disabled : null, active ? variantClasses.active : null, - ...aphroditeStyles + ...cssStyles ); if (className) { props.className += (' ' + className); @@ -64,23 +64,23 @@ class Button extends Component { Button.propTypes = { active: PropTypes.bool, - aphroditeStyles: PropTypes.arrayOf(PropTypes.shape({ - _definition: PropTypes.object, - _name: PropTypes.string, - })), block: PropTypes.bool, color: PropTypes.oneOf(BUTTON_COLORS), component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string, ]), + cssStyles: PropTypes.arrayOf(PropTypes.shape({ + _definition: PropTypes.object, + _name: PropTypes.string, + })), disabled: PropTypes.bool, href: PropTypes.string, size: PropTypes.oneOf(BUTTON_SIZES), variant: PropTypes.oneOf(BUTTON_VARIANTS), }; Button.defaultProps = { - aphroditeStyles: [], + cssStyles: [], color: 'default', variant: 'fill', }; diff --git a/admin/client/App/elemental/FormField/index.js b/admin/client/App/elemental/FormField/index.js index 2a41aec77b..c5363e5019 100644 --- a/admin/client/App/elemental/FormField/index.js +++ b/admin/client/App/elemental/FormField/index.js @@ -17,7 +17,7 @@ class FormField extends Component { render () { const { formLayout = 'basic', labelWidth } = this.context; const { - aphroditeStyles, + cssStyles, children, className, cropLabel, @@ -31,7 +31,7 @@ class FormField extends Component { classes.FormField, classes['FormField--form-layout-' + formLayout], offsetAbsentLabel ? classes['FormField--offset-absent-label'] : null, - aphroditeStyles + cssStyles ); if (className) { props.className += (' ' + className); @@ -75,12 +75,12 @@ FormField.childContextTypes = { formFieldId: PropTypes.string, }; FormField.propTypes = { - aphroditeStyles: PropTypes.oneOfType([ + children: PropTypes.node, + cropLabel: PropTypes.bool, + cssStyles: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.shape(stylesShape)), PropTypes.shape(stylesShape), ]), - children: PropTypes.node, - cropLabel: PropTypes.bool, htmlFor: React.PropTypes.string, label: React.PropTypes.string, offsetAbsentLabel: React.PropTypes.bool, diff --git a/admin/client/App/elemental/FormInput/index.js b/admin/client/App/elemental/FormInput/index.js index 5a22203d21..cc03405c2a 100644 --- a/admin/client/App/elemental/FormInput/index.js +++ b/admin/client/App/elemental/FormInput/index.js @@ -15,7 +15,7 @@ class FormInput extends Component { } render () { const { - aphroditeStyles, + cssStyles, className, disabled, id, @@ -36,7 +36,7 @@ class FormInput extends Component { classes['FormInput__size--' + size], disabled ? classes['FormInput--disabled'] : null, formLayout ? classes['FormInput--form-layout-' + formLayout] : null, - ...concatClassnames(aphroditeStyles) + ...concatClassnames(cssStyles) ); if (className) { props.className += (' ' + className); @@ -61,7 +61,7 @@ const stylesShape = { }; FormInput.propTypes = { - aphroditeStyles: PropTypes.oneOfType([ + cssStyles: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.shape(stylesShape)), PropTypes.shape(stylesShape), ]), diff --git a/admin/client/App/elemental/FormInput/noedit.js b/admin/client/App/elemental/FormInput/noedit.js index eda86bb033..a4f4287768 100644 --- a/admin/client/App/elemental/FormInput/noedit.js +++ b/admin/client/App/elemental/FormInput/noedit.js @@ -56,7 +56,6 @@ const classes = { borderWidth: theme.input.border.width, color: theme.color.gray80, display: 'inline-block', - height: theme.input.height, lineHeight: theme.input.lineHeight, padding: `0 ${theme.input.paddingHorizontal}`, transition: 'border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s', diff --git a/admin/client/App/elemental/FormLabel/index.js b/admin/client/App/elemental/FormLabel/index.js index accaaa44a3..de50ee137f 100644 --- a/admin/client/App/elemental/FormLabel/index.js +++ b/admin/client/App/elemental/FormLabel/index.js @@ -3,7 +3,7 @@ import React, { PropTypes } from 'react'; import classes from './styles'; function FormLabel ({ - aphroditeStyles, + cssStyles, className, component: Component, cropText, @@ -20,7 +20,7 @@ function FormLabel ({ classes.FormLabel, formLayout ? classes['FormLabel--form-layout-' + formLayout] : null, cropText ? classes['FormLabel--crop-text'] : null, - aphroditeStyles + cssStyles ); if (className) { props.className += (' ' + className); @@ -41,15 +41,15 @@ const stylesShape = { }; FormLabel.propTypes = { - aphroditeStyles: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.shape(stylesShape)), - PropTypes.shape(stylesShape), - ]), component: PropTypes.oneOfType([ PropTypes.string, PropTypes.func, ]), cropText: PropTypes.bool, + cssStyles: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.shape(stylesShape)), + PropTypes.shape(stylesShape), + ]), }; FormLabel.defaultProps = { component: 'label', diff --git a/admin/client/App/elemental/Glyph/index.js b/admin/client/App/elemental/Glyph/index.js index faf2cb80b3..74300c9306 100644 --- a/admin/client/App/elemental/Glyph/index.js +++ b/admin/client/App/elemental/Glyph/index.js @@ -10,7 +10,7 @@ import classes from './styles'; // font and CSS; inflating the project size function Glyph ({ - aphroditeStyles, + cssStyles, className, color, component: Component, @@ -24,7 +24,7 @@ function Glyph ({ classes.glyph, colorIsValidType && classes['color__' + color], classes['size__' + size], - aphroditeStyles + cssStyles ) + ` ${octicons[name]}`; if (className) { props.className += (' ' + className); @@ -40,14 +40,14 @@ function Glyph ({ }; Glyph.propTypes = { - aphroditeStyles: PropTypes.shape({ - _definition: PropTypes.object, - _name: PropTypes.string, - }), color: PropTypes.oneOfType([ PropTypes.oneOf(Object.keys(colors)), PropTypes.string, // support random color strings ]), + cssStyles: PropTypes.shape({ + _definition: PropTypes.object, + _name: PropTypes.string, + }), name: PropTypes.oneOf(Object.keys(octicons)).isRequired, size: PropTypes.oneOf(Object.keys(sizes)), }; diff --git a/admin/client/App/elemental/GlyphButton/index.js b/admin/client/App/elemental/GlyphButton/index.js index 0e74b8527a..af9135a410 100644 --- a/admin/client/App/elemental/GlyphButton/index.js +++ b/admin/client/App/elemental/GlyphButton/index.js @@ -28,7 +28,7 @@ function GlyphButton ({ const icon = ( + {isLeft && icon} {children} {isRight && icon} diff --git a/admin/client/App/elemental/InlineGroup/index.js b/admin/client/App/elemental/InlineGroup/index.js index b7a5027339..f6626c4615 100644 --- a/admin/client/App/elemental/InlineGroup/index.js +++ b/admin/client/App/elemental/InlineGroup/index.js @@ -4,7 +4,7 @@ import React, { cloneElement, Children, PropTypes } from 'react'; // NOTE: only accepts InlineGroupSection as a single child function InlineGroup ({ - aphroditeStyles, + cssStyles, block, children, className, @@ -16,7 +16,7 @@ function InlineGroup ({ props.className = css( classes.group, !!block && classes.block, - aphroditeStyles + cssStyles ); if (className) { props.className += (' ' + className); @@ -28,7 +28,7 @@ function InlineGroup ({ // normalize the count const count = buttons.length - 1; - // clone children and apply classNames that aphrodite can target + // clone children and apply classNames that glamor can target props.children = buttons.map((c, idx) => { if (!c) return null; @@ -53,16 +53,16 @@ function InlineGroup ({ }; InlineGroup.propTypes = { - aphroditeStyles: PropTypes.shape({ - _definition: PropTypes.object, - _name: PropTypes.string, - }), block: PropTypes.bool, component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string, ]), contiguous: PropTypes.bool, + cssStyles: PropTypes.shape({ + _definition: PropTypes.object, + _name: PropTypes.string, + }), }; InlineGroup.defaultProps = { component: 'div', diff --git a/admin/client/App/elemental/InlineGroupSection/index.js b/admin/client/App/elemental/InlineGroupSection/index.js index a836dc936a..b6cf3b55ff 100644 --- a/admin/client/App/elemental/InlineGroupSection/index.js +++ b/admin/client/App/elemental/InlineGroupSection/index.js @@ -6,7 +6,7 @@ import classes from './styles'; function InlineGroupSection ({ active, - aphroditeStyles, + cssStyles, children, className, contiguous, @@ -20,19 +20,19 @@ function InlineGroupSection ({ // A `contiguous` section must manipulate it's child directly // A separate (default) section just wraps the child return contiguous ? cloneElement(children, { - aphroditeStyles: [ + cssStyles: [ classes.contiguous, classes['contiguous__' + position], active ? classes.active : null, grow ? classes.grow : null, - aphroditeStyles, + cssStyles, ], ...props, }) : (
{children}
diff --git a/admin/client/App/elemental/LoadingButton/index.js b/admin/client/App/elemental/LoadingButton/index.js index eaec19dbf8..c9da922513 100644 --- a/admin/client/App/elemental/LoadingButton/index.js +++ b/admin/client/App/elemental/LoadingButton/index.js @@ -34,7 +34,7 @@ function LoadingButton ({ children, loading, ...props }) { : 0, }; - // render all that shit + // render everything return ( @@ -135,14 +137,15 @@ const classes = { marginTop: 10, position: 'absolute', left: 0, - zIndex: 2, + zIndex: 500, }, swatch: { borderRadius: 1, - boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.1)', + boxShadow: '0 0 0 1px rgba(0,0,0,0.1)', display: 'block', - height: '100%', - width: '100%', + ' svg': { + display: 'block', + }, }, }; diff --git a/fields/types/color/colored-swatch.js b/fields/types/color/colored-swatch.js new file mode 100644 index 0000000000..ead482a7f6 --- /dev/null +++ b/fields/types/color/colored-swatch.js @@ -0,0 +1,7 @@ +module.exports = ( + ` + + + + ` +); diff --git a/fields/types/date/DateColumn.js b/fields/types/date/DateColumn.js index d3c4880da0..a009d3a646 100644 --- a/fields/types/date/DateColumn.js +++ b/fields/types/date/DateColumn.js @@ -10,12 +10,19 @@ var DateColumn = React.createClass({ data: React.PropTypes.object, linkTo: React.PropTypes.string, }, + toMoment (value) { + if (this.props.col.field.isUTC) { + return moment.utc(value); + } else { + return moment(value); + } + }, getValue () { const value = this.props.data.fields[this.props.col.path]; if (!value) return null; const format = (this.props.col.type === 'datetime') ? 'MMMM Do YYYY, h:mm:ss a' : 'MMMM Do YYYY'; - return moment(value).format(format); + return this.toMoment(value).format(format); }, render () { const value = this.getValue(); diff --git a/fields/types/date/DateField.js b/fields/types/date/DateField.js index 07ea682997..3bb5661bb5 100644 --- a/fields/types/date/DateField.js +++ b/fields/types/date/DateField.js @@ -28,6 +28,7 @@ module.exports = Field.create({ note: React.PropTypes.string, onChange: React.PropTypes.func, path: React.PropTypes.string, + todayButton: React.PropTypes.bool, value: React.PropTypes.string, }, @@ -85,9 +86,12 @@ module.exports = Field.create({ value={value} /> -
- -
+ { + this.props.todayButton + &&
+ +
+ } ); }, diff --git a/fields/types/date/DateType.js b/fields/types/date/DateType.js index 11499a067d..5d25164900 100644 --- a/fields/types/date/DateType.js +++ b/fields/types/date/DateType.js @@ -13,12 +13,13 @@ function date (list, path, options) { this._nativeType = Date; this._underscoreMethods = ['format', 'moment', 'parse']; this._fixedSize = 'medium'; - this._properties = ['formatString', 'yearRange', 'isUTC', 'inputFormat']; + this._properties = ['formatString', 'yearRange', 'isUTC', 'inputFormat', 'todayButton']; this.parseFormatString = options.inputFormat || 'YYYY-MM-DD'; this.formatString = (options.format === false) ? false : (options.format || 'Do MMM YYYY'); this.yearRange = options.yearRange; this.isUTC = options.utc || false; + this.todayButton = typeof options.todayButton !== 'undefined' ? options.todayButton : true; /* * This offset is used to determine whether or not a stored date is probably corrupted or not. diff --git a/fields/types/date/Readme.md b/fields/types/date/Readme.md index 4e6579cce3..8006694fc1 100644 --- a/fields/types/date/Readme.md +++ b/fields/types/date/Readme.md @@ -16,17 +16,26 @@ String parsing with moment will be done using the `inputFormat` option, which de ``` ## Options +* `inputFormat` `String` -* `format` `String` +How the field interpret string input. See moment documentation for more information on available options. + +Defaults to 'YYYY-MM-DD' -The default format pattern to use, defaults to Do MMM YYYY +* `format` `String` -* `yearRange` `Array` `minYear, maxYear` +The default format pattern data will be returned in from the database. -The default range of years to be displayed. +Defaults to 'Do MMM YYYY' See the [momentjs format docs](http://momentjs.com/docs/#/displaying/format/) for information on the supported formats and options. +* `todayButton` `Boolean` + +Determines if the Today button will be displayed. + +Defaults to 'true' + ## Methods ### `format(formatString)` diff --git a/fields/types/datearray/Readme.md b/fields/types/datearray/Readme.md index 77c2f485f3..3939d06092 100644 --- a/fields/types/datearray/Readme.md +++ b/fields/types/datearray/Readme.md @@ -1,6 +1,9 @@ # Datearray field Stores an `Array` of `Dates` in the model. +In the admin UI displays a Date field, with an 'add item' button below it. + +Each item in the date array is validated and parsed using [momentjs](momentjs.com). See the moment documents for what valid formats are. ## Options @@ -12,11 +15,11 @@ Defaults to `" | "`; ### `parseFormat` `String` -The default date format, defaults to `"'YYYY-MM-DD'"` +The default format used to validate information being added. Defaults to `"'YYYY-MM-DD'"`. Uses moment to parse the input with the format string. ### `format` `String` -The default format pattern to use, defaults to `"'Do MMM YYYY'"` +The default format to display information in. Defaults to `"'Do MMM YYYY'"`. This is parsed using moment. ## Methods diff --git a/fields/types/datearray/test/explorer.js b/fields/types/datearray/test/explorer.js index e47b5a759c..5cebddbfb3 100644 --- a/fields/types/datearray/test/explorer.js +++ b/fields/types/datearray/test/explorer.js @@ -1,6 +1,6 @@ module.exports = { - Field: require('../DatearrayField'), - Filter: require('../DatearrayFilter'), + Field: require('../DateArrayField'), + Filter: require('../DateArrayFilter'), section: 'Date', spec: { label: 'Datearray', diff --git a/fields/types/datetime/Readme.md b/fields/types/datetime/Readme.md index 3e8c001285..d408de5a87 100644 --- a/fields/types/datetime/Readme.md +++ b/fields/types/datetime/Readme.md @@ -1,7 +1,7 @@ -# `DateTime` Field +# DateTime Field Stores a `String` of both date and time in the model. -Displayed as a date and time picker in the Admin UI. +Displayed as a date and time picker in the Admin UI. Internally uses [moment.js](http://momentjs.com/) to manage date parsing, formatting and comparison. @@ -17,12 +17,23 @@ String parsing with moment will be done using the `parseFormat` option, which de ## Options +* `parseFormat` `string` + +The default pattern to read in values with. Defaults to an array of values to try: + +`['YYYY-MM-DD', 'YYYY-MM-DD h:m:s a', 'YYYY-MM-DD h:m a', 'YYYY-MM-DD H:m:s', 'YYYY-MM-DD H:m', 'YYYY-MM-DD h:mm:s a Z', moment.ISO_8601]` + + * `format` `string` -The default format pattern to use, defaults to `Do MMM YYYY hh:mm:ss a` +The default format pattern to use when display the information. Defaults to `Do MMM YYYY hh:mm:ss a` See the [momentjs format docs](http://momentjs.com/docs/#/displaying/format/) for information on the supported formats and options. +`utc` `boolean` + +Sets whether the string should be displayed in the admin UI in UTC time or local time. Defaults to `false`. + ## Methods ### `updateItem` diff --git a/fields/types/embedly/EmbedlyType.js b/fields/types/embedly/EmbedlyType.js index b0e2cad1a7..2bfa7d9a3a 100644 --- a/fields/types/embedly/EmbedlyType.js +++ b/fields/types/embedly/EmbedlyType.js @@ -221,24 +221,29 @@ embedly.prototype.inputIsValid = function () { embedly.prototype.updateItem = function (item, data, callback) { // TODO: This could be more granular and check for actual changes to values, // see the Location field for an example - item.set(item.set(this.path, { - exists: data[this.paths.exists], - type: data[this.paths.type], - title: data[this.paths.title], - url: data[this.paths.url], - width: data[this.paths.width], - height: data[this.paths.height], - version: data[this.paths.version], - description: data[this.paths.description], - html: data[this.paths.html], - authorName: data[this.paths.authorName], - authorUrl: data[this.paths.authorUrl], - providerName: data[this.paths.providerName], - providerUrl: data[this.paths.providerUrl], - thumbnailUrl: data[this.paths.thumbnailUrl], - thumbnailWidth: data[this.paths.thumbnailWidth], - thumbnailHeight: data[this.paths.thumbnailHeight], - })); + + // This field type is never editable, so to ensure that we don't inadvertently reset the fields on this item with a null value + // A conditional has been added to negate updating this item should the fromPath on the passed in data object be the same as that on the item. + if (data[this.fromPath] !== item[this.fromPath]) { + item.set(item.set(this.path, { + exists: data[this.paths.exists], + type: data[this.paths.type], + title: data[this.paths.title], + url: data[this.paths.url], + width: data[this.paths.width], + height: data[this.paths.height], + version: data[this.paths.version], + description: data[this.paths.description], + html: data[this.paths.html], + authorName: data[this.paths.authorName], + authorUrl: data[this.paths.authorUrl], + providerName: data[this.paths.providerName], + providerUrl: data[this.paths.providerUrl], + thumbnailUrl: data[this.paths.thumbnailUrl], + thumbnailWidth: data[this.paths.thumbnailWidth], + thumbnailHeight: data[this.paths.thumbnailHeight], + })); + } process.nextTick(callback); }; diff --git a/fields/types/embedly/Readme.md b/fields/types/embedly/Readme.md index 1f7ea2f923..5d5367d71b 100644 --- a/fields/types/embedly/Readme.md +++ b/fields/types/embedly/Readme.md @@ -8,7 +8,7 @@ It stores the retrieved data (which includes the provider, media type, full URL, The API call to retrieve the data is implemented as a pre-save hook, and is only triggered if the `from path` value has changed. -See the [Embed.ly configuration documentation](http://keystonejs.com/docs/configuration#services-embedly) for details on how to set up Embed.ly in KeystoneJS. +See the [Embed.ly configuration documentation](/configuration#embedly) for details on how to set up Embed.ly in KeystoneJS. ```js { type: Types.Embedly, from: 'path' } diff --git a/fields/types/file/FileField.js b/fields/types/file/FileField.js index 89d36408f0..7b0981ec3b 100644 --- a/fields/types/file/FileField.js +++ b/fields/types/file/FileField.js @@ -14,6 +14,7 @@ import { } from '../../../admin/client/App/elemental'; import FileChangeMessage from '../../components/FileChangeMessage'; import HiddenFileInput from '../../components/HiddenFileInput'; +import ImageThumbnail from '../../components/ImageThumbnail'; let uploadInc = 1000; @@ -31,6 +32,7 @@ module.exports = Field.create({ label: PropTypes.string, note: PropTypes.string, path: PropTypes.string.isRequired, + thumb: PropTypes.bool, value: PropTypes.shape({ filename: PropTypes.string, // TODO: these are present but not used in the UI, @@ -73,6 +75,13 @@ module.exports = Field.create({ ? this.state.userSelectedFile.name : this.props.value.filename; }, + getFileUrl () { + return this.props.value && this.props.value.url; + }, + isImage () { + const href = this.props.value ? this.props.value.url : undefined; + return href && href.match(/\.(jpeg|jpg|gif|png|svg)$/i) != null; + }, // ============================== // METHODS @@ -126,7 +135,7 @@ module.exports = Field.create({ return (
{(this.hasFile() && !this.state.removeExisting) ? ( - + {this.getFilename()} ) : null} @@ -190,23 +199,44 @@ module.exports = Field.create({ return null; } }, + renderImagePreview () { + const imageSource = this.getFileUrl(); + return ( + + + + ); + }, renderUI () { - const { label, note, path } = this.props; + const { label, note, path, thumb } = this.props; + const isImage = this.isImage(); + const hasFile = this.hasFile(); + + const previews = ( +
+ {isImage && thumb && this.renderImagePreview()} + {hasFile && this.renderFileNameAndChangeMessage()} +
+ ); const buttons = ( -
+
- {this.hasFile() && this.renderClearButton()} + {hasFile && this.renderClearButton()}
); - return (
{this.shouldRenderField() ? (
- {this.hasFile() && this.renderFileNameAndChangeMessage()} + {previews} {buttons} ) : (
- {this.hasFile() + {hasFile ? this.renderFileNameAndChangeMessage() : no file}
diff --git a/fields/types/file/Readme.md b/fields/types/file/Readme.md index 8333868f94..4b69d00337 100644 --- a/fields/types/file/Readme.md +++ b/fields/types/file/Readme.md @@ -2,6 +2,8 @@ The File fields stores a file using Keystone Storage and a Storage Adapter (e.g. `FS`, `S3`, etc). You have to configure a `Storage` instance first then provide it in the options for the field, e.g. +Storage adapters are built per field. Look up the documentation on the individual adapters. + ```js var storage = new keystone.Storage({ adapter: keystone.Storage.Adapters.FS, @@ -30,10 +32,6 @@ The field stores a nested `Object` in the model. The nested schema is based on t Different adapters may add additional paths to the field schema - see the documentation for the Adapter you're using for more information. -## Options - -> TODO - ## Updates ```js @@ -110,10 +108,6 @@ To reset the field value _without_ deleting the stored file, provide an empty / ## Methods -### `format` - -> TODO - ### `upload` This method uploads a file using your storage provider. You can call it directly on the list: @@ -149,13 +143,8 @@ There is no way to upload directly from a buffer at the moment, you must upload ### `remove` -> TODO +Calls the `removeFile` on the storage adapter provided. ### `reset` -> TODO - - -## Filtering - -> TODO +Resets all fields in the storage schema. diff --git a/fields/types/geopoint/Readme.md b/fields/types/geopoint/Readme.md index f7492569f4..2dbd084844 100644 --- a/fields/types/geopoint/Readme.md +++ b/fields/types/geopoint/Readme.md @@ -2,7 +2,9 @@ Stores an `Array` of `Number` values in the model. -Displayed as two text input fields in the Admin UI. +Displayed as two text input fields in the Admin UI of latitude and longitude. + +If you are updating the database, it requires the two numbers to be in [longitude, latitude] order. ## Example ```js diff --git a/fields/types/geopoint/test/explorer.js b/fields/types/geopoint/test/explorer.js index 1ccf071633..ed5435be84 100644 --- a/fields/types/geopoint/test/explorer.js +++ b/fields/types/geopoint/test/explorer.js @@ -1,6 +1,6 @@ module.exports = { - Field: require('../GeopointField'), - Filter: require('../GeopointFilter'), + Field: require('../GeoPointField'), + Filter: require('../GeoPointFilter'), section: 'Miscellaneous', spec: { label: 'Geopoint', diff --git a/fields/types/localfile/Readme.md b/fields/types/localfile/Readme.md index ee3e90a575..df2c9edfa6 100644 --- a/fields/types/localfile/Readme.md +++ b/fields/types/localfile/Readme.md @@ -1,5 +1,7 @@ # LocalFile Field +> Warning: the LocalFile Field has been deprecated. Please use the [File](/api/field/File) and a storage adapter going forward. + `Object` — Displayed as a file upload field in the Admin UI Stores files on the local file system. diff --git a/fields/types/localfiles/readme.md b/fields/types/localfiles/readme.md new file mode 100644 index 0000000000..02a699fc88 --- /dev/null +++ b/fields/types/localfiles/readme.md @@ -0,0 +1,3 @@ +# LocalFiles + +> Warning: the LocalFiles Field has been deprecated. Please use the [File](/api/field/File) and a storage adapter going forward. diff --git a/fields/types/location/LocationType.js b/fields/types/location/LocationType.js index db952c454a..e7cc0bbac0 100644 --- a/fields/types/location/LocationType.js +++ b/fields/types/location/LocationType.js @@ -320,8 +320,8 @@ location.prototype.updateItem = function (item, data, callback) { if (doGoogleLookup) { var googleUpdateMode = this.getValueFromData(data, '_improve_overwrite') ? 'overwrite' : true; this.googleLookup(item, false, googleUpdateMode, function (err, location, result) { - // TODO: we are currently discarding the error; it should probably be - // sent back in the response, needs consideration + // TODO: we are currently log the error but otherwise discard it; should probably be returned.. needs consideration + if (err) console.error(err); callback(); }); return; @@ -431,7 +431,8 @@ location.prototype.googleLookup = function (item, region, update, callback) { _.forEach(result.address_components, function (val) { if (_.indexOf(val.types, 'street_number') >= 0) { - location.street1 = [val.long_name]; + location.street1 = location.street1 || []; + location.street1.unshift(val.long_name); } if (_.indexOf(val.types, 'route') >= 0) { location.street1 = location.street1 || []; @@ -450,6 +451,23 @@ location.prototype.googleLookup = function (item, region, update, callback) { if (_.indexOf(val.types, 'postal_code') >= 0) { location.postcode = val.short_name; } + + // These address_components could arguable all map to our 'number' field + // .. https://developers.google.com/maps/documentation/geocoding/intro#GeocodingResponses + + // `subpremise` - "Indicates a first-order entity below a named location, usually a singular building within a collection of buildings with a common name" + // In practice this is often the unit/apartment number or level and is not always included + if (_.indexOf(val.types, 'subpremise') >= 0) { + location.number = val.short_name; + } + + // These are all optional (rarely used?) and probably shouldn't replace the number if already set (due to subpremise) + // `floor` - Indicates the floor of a building address. + // `post_box` - Indicates a specific postal box. + // `room` - Indicates the room of a building address. + if (_.indexOf(val.types, 'floor') >= 0 || _.indexOf(val.types, 'post_box') >= 0 || _.indexOf(val.types, 'room') >= 0) { + location.number = location.number || val.short_name; + } }); if (Array.isArray(location.street1)) { diff --git a/fields/types/location/Readme.md b/fields/types/location/Readme.md index cb9ba0b33b..1cc0834cf8 100644 --- a/fields/types/location/Readme.md +++ b/fields/types/location/Readme.md @@ -40,6 +40,20 @@ Google Places integration requires the `google api key` option to be set for Key > Important: as per the MongoDB convention, the order for the geo array must be lng, lat which is the opposite of the order used by Google's API. +`enableImprove` `boolean` + +Options sets `enableMapsAPI` to true. If it is not set, `enableMapsAPI` is set to true if `google server api key` is set in keystone. + +`required` `Array or String or Boolean` + +Required works differently for location than for most other properties. There are three different types of require. + +If passed an `array`, it uses it to set which parts of the location field are required. + +If passed a comma-separated-value `string`, it will transform it into an array of required parts of the location field. + +If any positive value is passed in, the location field becomes required for validation, including either of the above options. + ## Underscore methods `googleLookup(region, update, callback)` - autodetect the full address and lng, lat from the stored value. diff --git a/fields/types/markdown/MarkdownField.js b/fields/types/markdown/MarkdownField.js index 6e69b415a0..bb304f4c96 100644 --- a/fields/types/markdown/MarkdownField.js +++ b/fields/types/markdown/MarkdownField.js @@ -109,6 +109,15 @@ var renderMarkdown = function (component) { $(component.refs.markdownTextarea).markdown(options); }; +// Simple escaping of html tags and replacing newlines for displaying the raw markdown string within an html doc +var escapeHtmlForRender = function (html) { + return html + .replace(/\&/g, '&') + .replace(/\/g, '>') + .replace(/\n/g, '
'); +}; + module.exports = Field.create({ displayName: 'MarkdownField', statics: { @@ -159,15 +168,11 @@ module.exports = Field.create({ }, renderValue () { - // TODO: victoriafrench - is this the correct way to do this? the object - // should be creating a default md where one does not exist imo. - - const innerHtml = ( - this.props.value !== undefined - && this.props.value.md !== undefined - ) - ? this.props.value.md.replace(/\n/g, '
') - : ''; + // We want to render the raw markdown string, without parsing it to html + // The markdown string *itself* may include html though so we need to escape it first + const innerHtml = (this.props.value && this.props.value.md) + ? escapeHtmlForRender(this.props.value.md) + : ''; return ( max) { - detail += 'password must not be longer than ' + max + ' characters\n'; + if (max && typeof pass === 'string' && pass.length > max) { + messages.push('Password must not be longer than ' + max + ' characters.'); } for (var prop in complexity) { - if (complexity[prop] && typeof passwordValue === 'string') { - var complexityCheck = (regexChunk[prop]).test(passwordValue); + if (complexity[prop] && typeof pass === 'string') { + var complexityCheck = (regexChunk[prop]).test(pass); if (!complexityCheck) { - detail += detailMsg[prop] + '\n'; + messages.push(detailMsg[prop]); } } } - result = detail.length === 0; - utils.defer(callback, result, detail); + if (pass && typeof pass === 'string' && rejectCommon && dumbPasswords.check(pass)) { + messages.push('Password must not be a common, frequently-used password.'); + } + + return { + result: messages.length === 0, + detail: messages.join(' \n'), + }; }; /** diff --git a/fields/types/password/Readme.md b/fields/types/password/Readme.md index 8d24c8160c..6f07c04a03 100644 --- a/fields/types/password/Readme.md +++ b/fields/types/password/Readme.md @@ -4,7 +4,7 @@ Stores a `String` in the model. Displayed as a password field in the Admin UI, with a 'change' button. -Passwords are automatically encrypted with bcrypt, and expose a method to compare a string to the encrypted hash. +Passwords are automatically encrypted with `bcrypt`, and expose a method to compare a string to the encrypted hash. > Note: The encryption happens with a **pre-save hook** added to the **schema**, so passwords set will not be encrypted until an item has been saved to the database. @@ -18,35 +18,62 @@ Passwords are automatically encrypted with bcrypt, and expose a method to compar `workFactor` `Number` -The bcrypt workfactor to use when generating the hash, higher numbers are slower but more secure (defaults to `10`). +Supplied as the `bcrypt` cost parameter; controls the computational cost of generating and validating a hash. +Higher values are slower but, since they take longer to generate, more secure against brute force attacks. + +Defaults to `10`. +At this level, a modern laptop (Late 2016 MacBook Pro, 3.3 GHz Intel Core i7) can produce around ~4 hashes/second. + +The `bcrypt` algorithim applies this value as a power of two. +As such, passwords with a workfactor of `11` will take twice as long to store and validate as those with a workfactor of `10`. + +Values lower than `4` are ignored by the underlying implementation (a value `10` is substituted). + +`min` `Number` + +Defines the minimum allowed password length in characters. + +Defaults to `8` in accordance with the [NIST Digital Identity Guidelines](http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf). + +`max` `Number` + +Defines the maximum allowed password length in characters. + +The `bcrypt` algorithm, used by this field, operates on a 72 byte value. +Most implementation (including [the one we use](https://www.npmjs.com/package/bcrypt-nodejs)), silently truncate the string provided if it exceeds this limit. +The `max` length option defaults to 72 characters in an attempt to align with this limit. + +> Note: If multi-byte (ie. non-ASCII) characters are allowed, it will be possible to exceed the 72 byte limit without triggering the 72 character validation limit. + +Can be set to `false` to disable the max length check. + +> Note: Disabling `max` or setting its value to >72 prevents validation errors but does not address the underlying algorithmic limitation. + +`rejectCommon` `Boolean` + +Controls whether values should be validated against a list of known-common passwords. + +Defaults to `true` in accordance with the [NIST Digital Identity Guidelines](http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf). + +Implemented with the [`dumb-passwords` package](https://www.npmjs.com/package/dumb-passwords) +which validates against 10,000 common passwords complied by [security analyst Mark Burnett](https://xato.net/10-000-top-passwords-6d6380716fe0). `complexity` `Object` Allows to set complexity requirements: * `digitChar` `Boolean` - when set to `true`, requires at least one digit -* `spChar` `Boolean` - when set to `true`, requires at least one from the following special characters: !, @, #, $, %, ^, &, \*, (, ), + +* `spChar` `Boolean` - when set to `true`, requires at least one from the following special characters: `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `+` * `asciiChar` `Boolean` - when set to `true`, allows only ASCII characters (from range U+0020--U+007E) * `lowChar` `Boolean` - when set to `true`, requires at least one lower case character * `upperChar` `Boolean` - when set to `true`, requires at least one upper case character -### Example +Example: ```js { type: Types.Password, complexity: { digitChar: true, asciiChar: true } } ``` -`max` `Number` - -Sets the maximum password length; defaults to 72, in accordance with [bcrypt](https://www.google.com/search?q=bcrypt+max+length), which truncates the password to the first 72 bytes. - -Can be set to `false` to disable the max length. - -> Note: Disabling `max` or setting its value to >72 does not override the bcrypt specification. - -`min` `Number` - -Defines the minimum password length; disabled by default. ## Underscore methods diff --git a/fields/types/password/test/type.js b/fields/types/password/test/type.js index 9aeee4669a..2cadc166ab 100644 --- a/fields/types/password/test/type.js +++ b/fields/types/password/test/type.js @@ -142,22 +142,22 @@ exports.testFieldType = function (List) { }); describe('validateInput', function () { - it('should validate a matching password- and confirm value', function (done) { + it('should validate a matching password and confirm value', function (done) { List.fields.password.validateInput({ - password: 'asdf', - password_confirm: 'asdf', + password: 'vasjdhb273r8ywbfeuygr2834ryfhwubsudfih', + password_confirm: 'vasjdhb273r8ywbfeuygr2834ryfhwubsudfih', }, function (result) { demand(result).be.true(); done(); }); }); - it('should validate emtpy string input', function (done) { + it('should invalidate empty string input', function (done) { List.fields.password.validateInput({ password: '', password_confirm: '', }, function (result) { - demand(result).be.true(); + demand(result).be.false(); done(); }); }); @@ -179,12 +179,12 @@ exports.testFieldType = function (List) { }); }); - it('should validate undefined confirmation value', function (done) { + it('should invalidate undefined confirmation value', function (done) { List.fields.password.validateInput({ password: 'something', password_confirm: undefined, }, function (result) { - demand(result).be.true(); + demand(result).be.false(); done(); }); }); @@ -346,7 +346,7 @@ exports.testFieldType = function (List) { digitChar: 'nodigits', }, function (result, detail) { demand(result).be.false(); - demand(detail).be('enter at least one digit\n'); + demand(detail).be('enter at least one digit'); done(); }); }); @@ -356,7 +356,7 @@ exports.testFieldType = function (List) { spChar: 'nospecialchars', }, function (result, detail) { demand(result).be.false(); - demand(detail).be('enter at least one special character\n'); + demand(detail).be('enter at least one special character'); done(); }); }); @@ -366,7 +366,7 @@ exports.testFieldType = function (List) { asciiChar: 'םגפשבך', }, function (result, detail) { demand(result).be.false(); - demand(detail).be('only ASCII characters are allowed\n'); + demand(detail).be('Password must be longer than 8 characters. \nonly ASCII characters are allowed'); done(); }); }); @@ -376,7 +376,7 @@ exports.testFieldType = function (List) { lowChar: 'NOLOWERCASE', }, function (result, detail) { demand(result).be.false(); - demand(detail).be('use at least one lower case character\n'); + demand(detail).be('use at least one lower case character'); done(); }); }); @@ -386,7 +386,7 @@ exports.testFieldType = function (List) { upperChar: 'nouppercase', }, function (result, detail) { demand(result).be.false(); - demand(detail).be('use at least one upper case character\n'); + demand(detail).be('use at least one upper case character'); done(); }); }); @@ -587,7 +587,7 @@ exports.testFieldType = function (List) { }, }); } catch (err) { - demand(err.message).eql('FieldType.Password: options - min must be set at a lower value than max.'); + demand(err.message).eql('FieldType.Password: options - maximum password length cannot be less than the minimum length.'); done(); } }); diff --git a/fields/types/relationship/RelationshipField.js b/fields/types/relationship/RelationshipField.js index e757bd8494..e4bdf88358 100644 --- a/fields/types/relationship/RelationshipField.js +++ b/fields/types/relationship/RelationshipField.js @@ -68,7 +68,7 @@ module.exports = Field.create({ } // check if filtering by id and item was already saved - if (fieldName === ':_id' && Keystone.item) { + if (fieldName === '_id' && Keystone.item) { filters[key] = Keystone.item.id; return; } @@ -190,8 +190,13 @@ module.exports = Field.create({ }, renderSelect (noedit) { + const inputName = this.getInputName(this.props.path); + const emptyValueInput = (this.props.many && (!this.state.value || !this.state.value.length)) + ? : null; return (
+ {/* This input ensures that an empty value is submitted when no related items are selected */} + {emptyValueInput} {/* This input element fools Safari's autocorrect in certain situations that completely break react-select */} Warning: the S3 File Field has been deprecated. Please use the [File](/api/field/File) and a storage adapter going forward. + + `Object` — Displayed as an file upload field in the Admin UI. Automatically manages files stored in [Amazon S3](http://aws.amazon.com/s3), including uploading and deleting. diff --git a/fields/types/s3file/S3FileType.js b/fields/types/s3file/S3FileType.js index e38eaee147..a153649cff 100644 --- a/fields/types/s3file/S3FileType.js +++ b/fields/types/s3file/S3FileType.js @@ -4,10 +4,13 @@ Deprecated. Using this field will now throw an error, and this code will be removed soon. See https://github.com/keystonejs/keystone/wiki/File-Fields-Upgrade-Guide + +TODO: this is used by keystone/admin/server/api/s3.js to generate headers, and should be factored out */ /* eslint-disable */ - +var _ = require('lodash'); +var assign = require('object-assign'); var loggedWarning = false; /** @@ -68,7 +71,7 @@ Object.defineProperty(s3file.prototype, 's3config', { */ s3file.prototype.addToSchema = function (schema) { - var knox = require('knox'); + var knox = require('knox-s3'); var field = this; var paths = this.paths = { @@ -333,7 +336,7 @@ s3file.prototype.generateHeaders = function (item, file, callback) { */ s3file.prototype.uploadFile = function (item, file, update, callback) { - var knox = require('knox'); + var knox = require('knox-s3'); var field = this; var path = field.options.s3path ? field.options.s3path + '/' : ''; var prefix = field.options.datePrefix ? moment().format(field.options.datePrefix) + '-' : ''; diff --git a/fields/types/select/Readme.md b/fields/types/select/Readme.md index ac5a34ef64..bafa8f7aec 100644 --- a/fields/types/select/Readme.md +++ b/fields/types/select/Readme.md @@ -2,6 +2,7 @@ Stores a `String` or `Number` in the model. Displayed as a select field in the Admin UI. +Does not allow for multiple items to be selected. If you want to provide multiple values, you can use `TextArray` or `NumberArray`, although neither will have the same constrained input. You can limit the options using a pre-save hook. ```js { type: Types.Select, options: 'first, second, third' } @@ -31,12 +32,13 @@ Ensures a value has been provided. Empty strings are never valid, even if specif ## Options -### `number` `Boolean` -when `true`, causes the value of the field to be stored as a `Number` instead of a `String` +### `number` +`Boolean` when `true`, causes the value of the field to be stored as a `Number` instead of a `String` ```js - { type: Types.Select, numeric: true, options: [{ value: 1, label: 'One' }, { value: 2, label: 'Two' } + { type: Types.Select, numeric: true, options: [{ value: 1, label: 'One' }, { value: 2, label: 'Two' }] } ``` + ### `emptyOption` `Boolean` when `undefined || true`, includes a blank value as the first option in the `