Skip to content

Commit

Permalink
pkp/pkp-lib#9527 Form fields migrated to storybook
Browse files Browse the repository at this point in the history
  • Loading branch information
jardakotesovec committed Dec 12, 2023
1 parent 140d383 commit ecf091f
Show file tree
Hide file tree
Showing 151 changed files with 4,699 additions and 10 deletions.
4 changes: 4 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import Tab from '@/components/Tabs/Tab.vue';
import Tabs from '@/components/Tabs/Tabs.vue';
import FloatingVue from 'floating-vue';

import VueScrollTo from 'vue-scrollto';

import '../src/styles/_import.less';
import '../src/styles/_global.less';
import {initializeRTL} from 'storybook-addon-rtl';
Expand Down Expand Up @@ -61,6 +63,8 @@ setup((app) => {
},
});

app.use(VueScrollTo);

app.component('Badge', Badge);
app.component('Dropdown', Dropdown);
app.component('Icon', Icon);
Expand Down
1 change: 0 additions & 1 deletion src/components/ActionPanel/ActionPanel.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const Default = {
const dialogStore = useDialogStore();

function openDeleteDialog() {
console.log('open dialog!');
dialogStore.openDialog({
name: 'deleteDialog',
title: 'Delete Incomplete Submissions',
Expand Down
50 changes: 50 additions & 0 deletions src/components/Form/Form.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';

import * as FormStories from './Form.stories.js';

<Meta of={FormStories} />{' '}

# Form

## Global Events

| Key | Description |
| --- | --- |
| `form-success` | When the form is successfully submitted. The payload will include the form ID and the server response from the successful form submission. This is usually the object that was added or edited. |
| `notify` | When an error is encountered during form submission. See [Notify](#/utilities/Notify). |

## Usage

Use this component to display a form. Typically you will generate all the required props from one of the `FormComponent` classes on the server side. These props can then be passed to the form.

```html
<pkp-form v-bind="formData" @set="set" />
```

Learn more about [server-side form components](https://docs.pkp.sfu.ca/dev/documentation/en/frontend-forms).

## Multi-page Forms

Multi-page forms have not proven to be useful. This feature may be removed in a future version. Use the [Steps](#/component/Steps) component for step-by-step workflows.

## Form Submission and Error Handling

The `action` prop of most `<Form>` components will be a URL to which it can submit a `PUT` or `POST` request to the application's REST API. Forms will handle the following responses from the API automatically.

- A `200` response when successful with a JSON object describing the entity that was added or edited.
- A `403` or `404` response when the server refuses the submission with a JSON object describing the error.
- A `400` response when a validation error occurs with a JSON object describing the validation errors. The `<Form>` component will map most REST API validation errors to the correct form field.

See the [API Documentation](https://docs.pkp.sfu.ca/dev/api) for the specification of errors.

## Groups and Group Descriptions

Fields can be organized into groups and given a group title and description. This should be done when fields benefit from the breakdown and when sufficient horizontal space is available.

If a form does not occupy the full workspace (~992px) because it is embedded in a tab or modal, avoid group titles and descriptions to ensure adequate space remains for the form fields.

## Conditional Display

Fields can be shown or hidden based on the value of another field. The `showWhen` prop is used to control conditional display. This prop is documented in the [FieldBase](#/component/Form/fields/FieldBase) example.

<ArgTypes />
128 changes: 128 additions & 0 deletions src/components/Form/Form.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Form from './Form.vue';
import {useContainerStateManager} from '@/composables/useContainerStateManager';
import FormBase from './mocks/form-base';
import FormMultilingual from './mocks/form-multilingual';
import FormGroups from './mocks/form-groups';
import FormUser from './mocks/form-user';
import FormConditionalDisplay from './mocks/form-conditional-display';

export default {
title: 'Forms/Form',
component: Form,
};

export const Base = {
render: (args) => ({
components: {PkpForm: Form},
setup() {
const {get, set, components} = useContainerStateManager();
set('example', FormBase);
return {args, get, set, components};
},
template: `
<PkpForm v-bind="components.example" @set="set" />
`,
}),

args: {},
};

export const Multilingual = {
render: (args) => ({
components: {PkpForm: Form},
setup() {
const {get, set, components} = useContainerStateManager();
set('example', FormMultilingual);
return {args, get, set, components};
},
template: `
<PkpForm v-bind="components.example" @set="set" />
`,
}),

args: {},
};

export const WithGroups = {
render: (args) => ({
components: {PkpForm: Form},
setup() {
const {get, set, components} = useContainerStateManager();
set('example', FormGroups);
return {args, get, set, components};
},
template: `
<PkpForm v-bind="components.example" @set="set" />
`,
}),

args: {},
};

export const WithPagination = {
render: (args) => ({
components: {PkpForm: Form},
setup() {
const {get, set, components} = useContainerStateManager();
set('example', FormUser);
return {args, get, set, components};
},
template: `
<PkpForm v-bind="components.example" @set="set" />
`,
}),

args: {},
};

export const ConditionalDisplay = {
render: (args) => ({
components: {PkpForm: Form},
setup() {
const {get, set, components} = useContainerStateManager();
set('example', FormConditionalDisplay);
return {args, get, set, components};
},
template: `
<PkpForm v-bind="components.example" @set="set" />
`,
}),

args: {},
};

export const WithErrors = {
render: (args) => ({
components: {PkpForm: Form},
setup() {
const {get, set, components} = useContainerStateManager();
set('example', {
...FormUser,
errors: {
email: ['Please provide a valid email address'],
affiliation: {
en: ['You must enter your affiliation in English.'],
fr_CA: ['You must enter your affiliation in French.'],
ar: ['You must enter your affiliation in Arabic.'],
},
'user-locales': ['You must select at least two options.'],
bio: {
fr_CA: [
'Please provide a bio statement to accompany your publication.',
],
},
country: ['Please select your country.'],
'mailing-address': [
'You must enter a mailing address where you can receive post.',
],
},
});
return {args, get, set, components};
},
template: `
<PkpForm v-bind="components.example" @set="set" />
`,
}),

args: {},
};
18 changes: 18 additions & 0 deletions src/components/Form/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,39 +75,57 @@ export default {
FormPage,
},
props: {
/** Used by a parent component, such as `Container`, to identify events emitted from the form and update the form props when necessary. */
id: String,
/** The method to use when submitting the form. This should match the API endpoint that will handle the form. It can be `POST` (create) or `PUT` (edit). */
method: {
type: String,
default() {
return '';
},
},
/** Where the form should be submitted. It should be a full URL (`http://...`) to the API endpoint where this form is handled. */
action: {
type: String,
default() {
return '';
},
},
/** A boolean indicating whether this form can be submitted. The save button will be disable if this is false. */
canSubmit: {
type: Boolean,
default() {
return true;
},
},
/** Key/value object of messages. The key is the field `name` and the value is an array of errors. Errors are generated during form submission and handled automatically, so this prop can be omitted in most cicumstances. */
errors: {
type: Object,
default() {
return {};
},
},
/** Array of form fields. This prop is typically configured on the server, using the `Form` and `Field` classes in the PHP application. */
fields: Array,
/** Array of form groups. See "Groups and Group Descriptions" below. */
groups: Array,
/** Key/value of hidden fields that should be submitted with the form. The key will be used as the field's `name` attribute. */
hiddenFields: Object,
/** Array of form pages. See "Multi-page Forms" below. */
pages: Array,
/** The primary locale for this form. This may be the primary locale of the journal/press, submission or site depending on the form. */
primaryLocale: String,
/** The locale(s) the form is currently being presented in. */
visibleLocales: Array,
/** The locale(s) supported by this form. If a form has multilingual fields, it will display a separate input control for each of these locales. */
supportedFormLocales: Array,
},
emits: [
/** When the form props need to be updated. The payload is an object with any keys that need to be modified. */
'set',
/** When the form has been successfully submitted. The payload will include the server response from the successful form submission. This is usually the object that was added or edited. */
'success',
],
data() {
return {
currentPage: '',
Expand Down
16 changes: 16 additions & 0 deletions src/components/Form/fields/FieldArchivingPn.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';

import * as FieldArchivingPnStories from './FieldArchivingPn.stories.js';

<Meta of={FieldArchivingPnStories} />

# FieldArchivingPn

## Usage

This component is a special field for the [PKP Preservation Network](https://pkp.sfu.ca/pkp-pn/) settings. It displays a button to open the plugin settings in a modal when the PKP PN plugin is enabled.

The modal will _not_ open in the demonstration above.

<Primary />
<Controls />
33 changes: 33 additions & 0 deletions src/components/Form/fields/FieldArchivingPn.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import FieldArchivingPn from './FieldArchivingPn.vue';

import FieldBaseMock from '../mocks/field-base';
import FieldArchivingPnMock from '../mocks/field-archiving-pn';

export default {
title: 'Forms/FieldArchivingPn',
component: FieldArchivingPn,
render: (args) => ({
components: {FieldArchivingPn},
setup() {
function change(name, prop, newValue, localeKey) {
if (localeKey) {
args[prop][localeKey] = newValue;
} else {
args[prop] = newValue;
}
}

return {args, change};
},
template: `
<FieldArchivingPn v-bind="args" @change="change" />
`,
}),
};

export const Default = {
args: {
...FieldBaseMock,
...FieldArchivingPnMock,
},
};
5 changes: 5 additions & 0 deletions src/components/Form/fields/FieldArchivingPn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@ export default {
name: 'FieldArchivingPn',
extends: FieldOptions,
props: {
/** The current value for this field. */
value: {
type: Boolean,
required: true,
},
/** An HTML string with a `<button>` that can be used to open the preservation plugin's settings modal. **Note: the modal will not open in this demonstration.** */
terms: {
type: String,
required: true,
Expand All @@ -93,14 +95,17 @@ export default {
type: String,
required: true,
},
/** A URL to send a request to the plugin grid handler to enable this plugin. */
enablePluginUrl: {
type: String,
required: true,
},
/** A URL to send a request to the plugin grid handler to disable this plugin. */
disablePluginUrl: {
type: String,
required: true,
},
/** A URL to send a request to the plugin grid handler to display the settings for this plugin. **Note: the modal will not open in this demonstration.** */
settingsUrl: {
type: String,
required: true,
Expand Down
21 changes: 21 additions & 0 deletions src/components/Form/fields/FieldAutosuggestPreset.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';

import * as FieldAutosuggestPresetStories from './FieldAutosuggestPreset.stories.js';

<Meta of={FieldAutosuggestPresetStories} />

# FieldAutosuggestPreset

## Usage

This is an implementation of [FieldBaseAutosuggest](#/component/Form/fields/FieldBaseAutosuggest) that does not require a request to an API endpoint. Instead, the list of options are passed in as a prop.

```html
<field-autosuggest-preset ... :options="[ { value: 1, label: "Articles" }, {
value: 2, label: "Reviews" }, ]">
```

Use this component when there is a limited number of possible suggestions and a simple match with the `label` and `value` are sufficient.

<Primary />
<Controls />
Loading

0 comments on commit ecf091f

Please sign in to comment.