@@ -299,39 +288,13 @@ export default { components: { NavGroup, }, - data() { - return { - openSubmenus: [], - }; - }, methods: { - /** - * Should submenu be displayed? - */ - displaySubmenu(component) { - return ( - this.openSubmenus.includes(component) || this.pathIncludes(component) - ); - }, - /** * Does the current route path include this component */ pathIncludes(component) { return this.$route.path.includes('/component/' + component); }, - - /** - * Open/close a submenu in the nav - */ - toggleSubmenu(component) { - if (this.openSubmenus.includes(component)) { - const i = component.indexOf(component); - this.openSubmenus.splice(i, 1); - } else { - this.openSubmenus.push(component); - } - }, }, }; @@ -426,9 +389,9 @@ a { margin-bottom: 3rem; margin-left: auto; margin-right: auto; - max-width: 50em; - font-size: @font-sml; - line-height: 1.7em; + padding-left: 2em; + padding-right: 2em; + max-width: 54em; p { margin-top: 1.5rem; @@ -445,8 +408,8 @@ a { background: #f9f9f9; border-radius: @radius; padding: 0.125em 0.25em; - color: #009e2b; - font-size: @font-tiny; + color: #156f2d; + font-size: @font-sml; } h2, @@ -500,8 +463,8 @@ a { background: #f9f9f9; border-radius: @radius; padding: 0.125em 0.25em; - color: #3fab5c; - font-size: @font-tiny; + color: #156f2d; + font-size: @font-sml; } } diff --git a/src/components/Composer/Composer.vue b/src/components/Composer/Composer.vue index 94b4fac78..39328d6cd 100644 --- a/src/components/Composer/Composer.vue +++ b/src/components/Composer/Composer.vue @@ -514,6 +514,9 @@ export default { }; }, computed: { + /** + * A getter and setter to allow v-model on a prop + */ bccBinded: { get() { return this.bcc; @@ -522,6 +525,11 @@ export default { this.emitChange({bcc: newVal}); }, }, + + /** + * init prop to pass to the the rich text field + * component for the message body + */ bodyInit() { if (!this.attachers.length) { return {}; @@ -540,6 +548,10 @@ export default { }, }; }, + + /** + * A getter and setter to allow v-model on a prop + */ ccBinded: { get() { return this.cc; @@ -548,6 +560,7 @@ export default { this.emitChange({cc: newVal}); }, }, + /** * Override the recipientName in the email variables * @@ -571,12 +584,24 @@ export default { } return variables; }, + + /** + * A unique id for the file attacher component + */ fileAttacherModalId() { return this.id + 'fileAttacher'; }, + + /** + * The first X items of email template search results + */ limitedSearchResults() { return this.searchResults.slice(0, this.showSearchResultCount); }, + + /** + * The recipient options with names in the current locale + */ localizedRecipientOptions() { const locale = this.locale ?? $.pkp.app.currentLocale; return this.recipientOptions.map((recipient) => { @@ -586,15 +611,21 @@ export default { }; }); }, + + /** + * The prepared content variables in the current locale + */ localizedVariables() { return this.variables[this.locale] ? this.variables[this.locale] : []; }, + /** - * A list of supported locales without the currently active locale + * A list of supported locales other than the currently active locale */ otherLocales() { return this.locales.filter((locale) => locale.locale !== this.locale); }, + /** * The names of all the recipients separated by a comma */ @@ -607,6 +638,10 @@ export default { } return name; }, + + /** + * A getter and setter to allow v-model on a prop + */ subjectBinded: { get() { return this.subject; @@ -615,6 +650,7 @@ export default { this.emitChange({subject: newVal}); }, }, + /** * The recipient options that are currently * set in the recipients array @@ -628,6 +664,9 @@ export default { methods: { /** * Add file attachments to the email + * + * @param {String} component The name of the `FileAttacher****` component used to attach files + * @param {Array} files The files attached to the email */ addAttachments(component, files) { const attachments = [ @@ -664,6 +703,10 @@ export default { /** * Respond to change events from the recipient autosuggest field + * + * @param {String} name The `name` of the input field + * @param {String} prop The property that was changed. + * @param {String} value The new value for the property */ changeRecipients(name, prop, value) { if (prop === 'value') { @@ -675,6 +718,7 @@ export default { * Get the icon to match this document type, * such as PDF, Word, spreadsheet, etc. * + * @param {Object} attachment The file attachment * @return {String} */ getDocumentTypeIcon(attachment) { @@ -687,6 +731,8 @@ export default { /** * Emit an event to change a prop + * + * @param {Object} data The props of this component that should change */ emitChange(data) { this.$emit('set', this.id, data); @@ -704,6 +750,8 @@ export default { /** * Get a plain text snippet of an email template's body + * + * @param {String} str An HTML string of the email template's body */ getBodySnippet(str) { const length = 70; @@ -718,6 +766,8 @@ export default { /** * Load an email template and update the subject/body + * + * @param {String} key The email template key */ loadTemplate(key) { this.isLoadingTemplate = true; @@ -768,6 +818,8 @@ export default { /** * Open a confirmation modal to change the locale + * + * @param {String} locale The requested locale */ openSwitchLocale(locale) { const localeName = this.locales.find( @@ -803,6 +855,8 @@ export default { /** * Remove a file attachment + * + * @param {Number} index The array index of the attachment to remove */ removeAttachment(index) { this.emitChange({ @@ -835,43 +889,46 @@ export default { return; } this.isSearching = true; - this.latestSearchRequest = $.pkp.classes.Helper.uuid(); + const uuid = $.pkp.classes.Helper.uuid(); + this.latestSearchRequest = uuid; this.showSearchResultCount = 10; - var self = this; $.ajax({ url: this.emailTemplatesApiUrl, type: 'GET', + context: this, data: { searchPhrase: this.searchPhrase, }, _uuid: this.latestSearchRequest, error: function (r) { // Only process latest request response - if (self.latestSearchRequest !== this._uuid) { + if (this.latestSearchRequest !== uuid) { return; } - self.ajaxErrorCallback(r); + this.ajaxErrorCallback(r); }, success: function (r) { // Only process latest request response - if (self.latestSearchRequest !== this._uuid) { + if (this.latestSearchRequest !== uuid) { return; } - self.searchResults = r.items; + this.searchResults = r.items; }, complete() { // Only process latest request response - if (self.latestSearchRequest !== this._uuid) { + if (this.latestSearchRequest !== uuid) { return; } - self.isSearching = false; + this.isSearching = false; }, }); }, /** * Emit an event to set the body of the email + * + * @param {String} value The new body value */ setBody(value) { this.emitChange({body: value}); @@ -879,6 +936,8 @@ export default { /** * Emit an event to set the subject of the email + * + * @param {String} value The new subject value */ setSubject(value) { this.emitChange({ @@ -889,6 +948,8 @@ export default { /** * Switch the current locale and load the default template * in the new locale + * + * @param {String} locale The locale key to switch to. Example: `en` */ switchLocale(locale) { this.emitChange({locale: locale}); @@ -896,7 +957,11 @@ export default { }, /** - * Update the padding of the to element to account for the CC button + * Update the padding of the "to" element to account for the CC button + * + * This makes sure that the input field does not run into the space occupied + * by the button to Add CC/BCC. The width of this button will change depending + * on the language, so it is calculated at run-time. */ updateToPadding() { const inputEl = this.$el.querySelector('.pkpAutosuggest__inputWrapper'); diff --git a/src/components/Container/DecisionPage.vue b/src/components/Container/DecisionPage.vue index 059f9c2a4..6a99049fe 100644 --- a/src/components/Container/DecisionPage.vue +++ b/src/components/Container/DecisionPage.vue @@ -36,7 +36,6 @@ export default { decisionCompleteLabel: '', decisionCompleteDescription: '', emailTemplatesApiUrl: '', - fileGenres: [], startedSteps: [], isSubmitting: false, keepWorkingLabel: '', @@ -52,9 +51,16 @@ export default { }; }, computed: { + /** + * The array index of the current step + */ currentStepIndex() { return this.steps.findIndex((step) => step.id === this.currentStep.id); }, + + /** + * Validation errors + */ errors() { return this.steps .filter( @@ -71,16 +77,25 @@ export default { return errors; }, {}); }, + + /** + * Is the current step the first step? + */ isOnFirstStep() { return 0 === this.currentStepIndex; }, + + /** + * Is the current step the last step? + */ isOnLastStep() { return this.currentStepIndex === this.steps.length - 1; }, }, methods: { /** - * Cancel the decision and return to the submission + * Open a confirmation prompt to cancel the + * decision and return to the submission */ cancel() { this.openDialog({ @@ -105,6 +120,10 @@ export default { /** * Copy a file to a new file stage + * + * @param {Number} fileId The submission file id + * @param {Number} toFileStage The file stage to copy the submission to + * @param {Function} callback A callback function to fire when the request finished successfully */ copyFile(fileId, toFileStage, callback) { $.ajax({ @@ -127,13 +146,6 @@ export default { }); }, - /** - * Get the genre of a submission file - */ - getFileGenre(genreId) { - return this.fileGenres.find((genre) => genre.id === genreId); - }, - /** * Go to the next step or submit if this is the last step */ @@ -174,6 +186,8 @@ export default { /** * Go to a step in the wizard + * + * @param {String} stepId The id of the step to go to */ openStep(stepId) { this.startedSteps = [...new Set([...this.startedSteps, stepId])]; @@ -194,6 +208,8 @@ export default { * Handle errors related to the decision request * * This method maps validation errors to their step. + * + * @param {Object} errors A key/value map of errors where each key represents a step index */ setStepErrors(errors) { this.steps.forEach((step, index) => { @@ -212,6 +228,8 @@ export default { /** * Skip a step and go to the next step or activate * a skipped step + * + * @param {String} stepId The id of the step */ toggleSkippedStep(stepId) { if (this.skippedSteps.includes(stepId)) { @@ -235,10 +253,13 @@ export default { /** * Submit an editorial decision + * + * This posts the editorial decison and copies + * all selected files when a file promotion step + * exists. */ submit() { this.isSubmitting = true; - let self = this; const steps = this.steps.filter( (step) => !this.skippedSteps.includes(step.id) ); @@ -293,6 +314,7 @@ export default { $.ajax({ url: this.submissionApiUrl + '/decisions', type: 'POST', + context: this, data: data, headers: { 'X-Csrf-Token': pkp.currentUser.csrfToken, @@ -301,37 +323,37 @@ export default { if (r.status && r.status === 400) { // The decision is invalid if (r.responseJSON.decision) { - self.ajaxErrorCallback({ + this.ajaxErrorCallback({ responseJSON: { errorMessage: r.responseJSON.decision[0], }, }); // An action is invalid } else if (r.responseJSON.actions) { - self.setStepErrors(r.responseJSON.actions); + this.setStepErrors(r.responseJSON.actions); } else { - self.ajaxErrorCallback(r); + this.ajaxErrorCallback(r); } } else { - self.ajaxErrorCallback(r); + this.ajaxErrorCallback(r); } - self.isSubmitting = false; + this.isSubmitting = false; }, success() { if (!files.length) { - self.isSubmitting = false; - self.openCompletedDialog(); + this.isSubmitting = false; + this.openCompletedDialog(); } let copiedCount = 0; const copyCompleted = () => { copiedCount++; if (copiedCount >= files.length) { - self.openCompletedDialog(); + this.openCompletedDialog(); clearInterval(copyCompleted); } }; files.forEach((file) => - self.copyFile(file.id, file.toFileStage, copyCompleted) + this.copyFile(file.id, file.toFileStage, copyCompleted) ); }, }); @@ -339,6 +361,9 @@ export default { /** * Update the data attached to a step + * + * @param {String} stepId The id of the step to update + * @param {Object} data The data to update in the step */ updateStep(stepId, data) { this.steps = this.steps.map((step) => { @@ -369,11 +394,15 @@ export default { }, }, created() { - // Start step 1 if (this.steps.length) { + /** + * Start the first step + */ this.openStep(this.steps[0].id); - // Set up email data for each email step + /** + * Set up email data for each email step + */ this.steps = this.steps.map((step) => { if (step.type !== stepTypes.email) { return step; diff --git a/src/components/Container/ManageEmailsPage.vue b/src/components/Container/ManageEmailsPage.vue index 4afa83851..f06f31570 100644 --- a/src/components/Container/ManageEmailsPage.vue +++ b/src/components/Container/ManageEmailsPage.vue @@ -20,7 +20,6 @@ export default { currentMailable: {}, currentTemplate: {}, currentTemplateForm: {}, - customTemplates: [], i18nRemoveTemplate: '', i18nRemoveTemplateMessage: '', i18nResetAll: '', @@ -34,6 +33,10 @@ export default { }; }, computed: { + /** + * Mailables currently visible in the list after + * search and filters applied + */ currentMailables() { let newMailables = [...this.mailables]; @@ -66,6 +69,12 @@ export default { }, }, methods: { + /** + * Add a filter to the active filters list + * + * @param {String} param The query param to effect + * @param {String|Number} value The value to add to the query param + */ addFilter(param, value) { let newFilters = {...this.activeFilters}; if (!newFilters[param]) { @@ -75,6 +84,12 @@ export default { } this.activeFilters = newFilters; }, + + /** + * Open a confirmation dialog to remove a template + * + * @param {Object} template The template to remove + */ confirmRemoveTemplate(template) { this.openDialog({ name: 'removeTemplate', @@ -109,6 +124,10 @@ export default { ], }); }, + + /** + * Open a confirmation dialog to reset all templates + */ confirmResetAll() { this.openDialog({ name: 'resetAll', @@ -141,6 +160,12 @@ export default { ], }); }, + + /** + * Open a confirmation dialog to reset a template + * + * @param {Object} template The template to remove + */ confirmResetTemplate(template) { this.openDialog({ name: 'resetTemplate', @@ -172,6 +197,14 @@ export default { ], }); }, + + /** + * Delete an email template + * + * @param {Object} template The template to remove + * @param {Function} onSuccess A callback function to fire when a success response is received + * @param {Function} onComplete A callback function to fire after any response is received + */ deleteTemplate(template, onSuccess, onComplete) { $.ajax({ url: this.templatesApiUrl + '/' + template.key, @@ -186,6 +219,13 @@ export default { complete: onComplete, }); }, + + /** + * Get a mailable from the API + * + * @param {Object} mailable The mailable to get from the API + * @param {Function} onSuccess A callback function to fire when a success response is received + */ getMailable(mailable, onSuccess) { $.ajax({ url: @@ -198,6 +238,13 @@ export default { success: onSuccess, }); }, + + /** + * Get an email template from the API + * + * @param {Object} template The template to get from the API + * @param {Function} onSuccess A callback function to fire when a success response is received + */ getTemplate(template, onSuccess) { $.ajax({ url: this.templatesApiUrl + '/' + encodeURIComponent(template), @@ -207,11 +254,22 @@ export default { success: onSuccess, }); }, + + /** + * Check if a filter is active + * + * @param {String} param The query param to check + * @param {String|Number} value The value of the query param to check + */ isFilterActive(param, value) { return ( this.activeFilters[param] && this.activeFilters[param].includes(value) ); }, + + /** + * Fired when the mailable modal is closed + */ mailableModalClosed() { this.resetFocus(); setTimeout(() => { @@ -219,6 +277,15 @@ export default { this.currentTemplate = {}; }, 300); }, + + /** + * Open the modal to edit a mailable + * + * If a mailable only has one template, this will open the + * email template modal instead. + * + * @param {Object} mailable The mailable to open + */ openMailable(mailable) { if (mailable.supportsTemplates) { this.getMailable(mailable, (mailable) => { @@ -233,12 +300,26 @@ export default { }); } }, + + /** + * Open the modal to edit an email template + * + * @param {Object} template The template open + */ openTemplate(template) { template = template || {}; this.resetFocusTo = document.activeElement; this.currentTemplate = template; this.$nextTick(() => this.$modal.show('template')); }, + + /** + * Remove a filter from the active filters list + * + * + * @param {String} param The query param to effect + * @param {String|Number} value The value to remove from the query param + */ removeFilter(param, value) { if (!this.activeFilters[param]) { return; @@ -247,11 +328,24 @@ export default { newFilters[param] = newFilters[param].filter((v) => v !== value); this.activeFilters = newFilters; }, + + /** + * A helper function to move the focus back to the element + * it was last at. This is usually used with modals to restore + * the focus after a modal is closed. + */ resetFocus() { if (this.resetFocusTo) { this.resetFocusTo.focus(); } }, + + /** + * Setup the form to edit an email template + * + * @param {Object} newTemplate The email template to set up the form. + * This will be empty if creating a new template. + */ setCurrentTemplateForm(newTemplate) { // Get a deep copy of the form to eliminate references let templateForm = JSON.parse(JSON.stringify(this.templateForm)); @@ -303,6 +397,12 @@ export default { this.currentTemplateForm = templateForm; }, + + /** + * Fired when the email template form has been saved + * + * @param {Object} template The updated values of the template + */ templateSaved(template) { const exists = this.currentMailable.emailTemplates.findIndex( @@ -320,6 +420,10 @@ export default { setTimeout(() => this.$modal.hide('template'), 1000); }, + + /** + * Fired when the email template modal has been closed + */ templateModalClosed() { if (this.currentMailable.supportsTemplates) { this.resetFocus(); @@ -330,6 +434,13 @@ export default { this.mailableModalClosed(); } }, + + /** + * Sync the form data with user input + * + * @param {String} formId The id for the form. Unused + * @param {Object} data The form data to be updated + */ updateCurrentTemplateForm(formId, data) { this.currentTemplateForm = { ...this.currentTemplateForm, @@ -338,6 +449,10 @@ export default { }, }, watch: { + /** + * Update the email template form whenever the current + * email template is changed + */ currentTemplate(newVal) { this.setCurrentTemplateForm(newVal); }, diff --git a/src/components/Container/Page.vue b/src/components/Container/Page.vue index ddaf9e2f6..f6a41595a 100644 --- a/src/components/Container/Page.vue +++ b/src/components/Container/Page.vue @@ -1,17 +1,9 @@ +``` + +## Localized Filenames + +When `` makes the `POST` request to upload a file, it will send a `name` param that matches the filename. + +```json +{ + "name": "the-uploaded-filename.ext" +} +``` + +If the name needs to be sent as [localized data](#/pages/localization), set the `filenameLocale` prop. + +```html + +``` + +The request body will change to this. + + +```json +{ + "name": { + "en": "the-uploaded-filename.ext" + } +} +``` diff --git a/src/docs/components/Filter/readme.md b/src/docs/components/Filter/readme.md index 6a55fd5b6..d42859f05 100644 --- a/src/docs/components/Filter/readme.md +++ b/src/docs/components/Filter/readme.md @@ -38,7 +38,7 @@ ## Usage -Use this component when the user wants to narrow the items in a list by some criteria. +Use this component when the user wants to narrow the items in a list by some criteria. This component is usually used with a [ListPanel](#/component/ListPanel). ## Using Filter Params and Values @@ -69,7 +69,7 @@ Most filters should be able to be combined with other filters, so there may be m ```json { - stageIds: [2, 3], - sectionIds: [1] + "stageIds": [2, 3], + "sectionIds": [1] } ``` diff --git a/src/docs/components/Form/fields/FieldBase/readme.md b/src/docs/components/Form/fields/FieldBase/readme.md index 84d04c3b1..fb7dbb2b7 100644 --- a/src/docs/components/Form/fields/FieldBase/readme.md +++ b/src/docs/components/Form/fields/FieldBase/readme.md @@ -24,10 +24,8 @@ | Key | Description | | --- | --- | -| `change` | Emitted when the value of the field changes. Payload: `(name, property, value, [localeKey])`. The `localeKey` will be null for fields that are not multilingual. This event is fired every time the value changes, so you should [debounce](https://www.npmjs.com/package/debounce) event callbacks that contain resource-intensive code. +| `change` | Emitted when a field prop changes. Payload: `(fieldName, propName, newValue, [localeKey])`. The `localeKey` will be null for fields that are not multilingual. This event is fired every time the `value` changes, so you should [debounce](https://www.npmjs.com/package/debounce) event callbacks that contain resource-intensive code. ## Usage -This is a base component for all `Field*` components. The props described above are available for all `Field*` components. - -This component should *not* be used directly. It is presented for demonstration purposes only. Use one of the other form field components. +This is a base component for all `` components. The props described above are available for all `` components. This component should *not* be used directly. It is presented for documentation purposes only. Use one of the other form field components. diff --git a/src/docs/components/Form/fields/FieldColor/readme.md b/src/docs/components/Form/fields/FieldColor/readme.md index 5fc92f93d..090dabd0f 100644 --- a/src/docs/components/Form/fields/FieldColor/readme.md +++ b/src/docs/components/Form/fields/FieldColor/readme.md @@ -11,4 +11,4 @@ See [FieldBase](#/component/Form/fields/FieldBase). ## Usage -Use this component when you want the user to select a hex color value. This component is only recommended for use in theme settings, but in special circumstances may be used elsewhere. +Use this component when you want the user to select a hex color value. This component is only recommended for use in theme settings. diff --git a/src/docs/components/Form/fields/FieldOptions/readme.md b/src/docs/components/Form/fields/FieldOptions/readme.md index b88127224..5da400cc1 100644 --- a/src/docs/components/Form/fields/FieldOptions/readme.md +++ b/src/docs/components/Form/fields/FieldOptions/readme.md @@ -18,9 +18,7 @@ Use this component for selecting one or more options from a list. If the list of ## Confirm fields and boolean values -When using this component as a confirmation field, the `value` prop passed to the field should be `true` or `false`. - -This differs from the field's normal `value` type, which is an array. +When using this component as a confirmation field, the `value` prop passed to the field should be `true` or `false`. This differs from the field's normal `value` type, which is an array. ## Orderable options diff --git a/src/docs/components/Form/fields/FieldPreparedContent/readme.md b/src/docs/components/Form/fields/FieldPreparedContent/readme.md index e75d3f1e0..c02a3044e 100644 --- a/src/docs/components/Form/fields/FieldPreparedContent/readme.md +++ b/src/docs/components/Form/fields/FieldPreparedContent/readme.md @@ -3,7 +3,7 @@ | Key | Description | | --- | --- | | `...` | Supports all props in [FieldRichTextarea](#/component/Form/fields/FieldRichTextarea). | -| `preparedContent` | An optional object containing preset information. When preset information exists, a button will appear in the toolbar. See the [Prepared Content](#/component/Form/fields/FieldPreparedContent) example. | +| `preparedContent` | An optional object containing preset information. When preset information exists, a button will appear in the toolbar. | ## Events @@ -11,7 +11,7 @@ See [FieldRichTextarea](#/component/Form/fields/FieldRichTextarea). ## Usage -Use this component to provide the user with a rich text editor with prepared content that they can insert through a modal UI. +Use this component to provide the user with a rich text editor with prepared content that they can insert through a modal. The `preparedContent` prop allows you to pass content to the editor and give the user a point-and-click tool to add that content. This is intended to be used in cases where data can be rendered inside of an email template. diff --git a/src/docs/components/Form/fields/FieldPubId/previews/PreviewFieldPubId.vue b/src/docs/components/Form/fields/FieldPubId/previews/PreviewFieldPubId.vue index 31f9e215d..8a7bf4006 100644 --- a/src/docs/components/Form/fields/FieldPubId/previews/PreviewFieldPubId.vue +++ b/src/docs/components/Form/fields/FieldPubId/previews/PreviewFieldPubId.vue @@ -6,7 +6,7 @@ import FieldPubId from '@/components/Form/fields/FieldPubId.vue'; import PreviewFieldBase from '../../FieldBase/previews/PreviewFieldBase.vue'; import fieldBase from '../../../helpers/field-base'; -import field from '../../../helpers/field-doi'; +import field from '../../../helpers/field-urn'; export default { extends: PreviewFieldBase, diff --git a/src/docs/components/Form/fields/FieldPubId/readme.md b/src/docs/components/Form/fields/FieldPubId/readme.md index 7fe422701..f61307ea7 100644 --- a/src/docs/components/Form/fields/FieldPubId/readme.md +++ b/src/docs/components/Form/fields/FieldPubId/readme.md @@ -22,4 +22,4 @@ See [FieldBase](#/component/Form/fields/FieldBase). ## Usage -This component is a special field for setting a pub id, like a DOI, based on pre-configured settings in a pub id plugin. When the plugin settings permit a pub id to be customized directly, a [FieldText](./FieldText) should be used instead. +This component is a special field for setting a pub id, like a URN, based on pre-configured settings in a pub id plugin. When the plugin settings permit a pub id to be customized directly, a [FieldText](./FieldText) should be used instead. diff --git a/src/docs/components/Form/fields/FieldRichText/ComponentFieldRichText.vue b/src/docs/components/Form/fields/FieldRichText/ComponentFieldRichText.vue new file mode 100644 index 000000000..3ea8c49c6 --- /dev/null +++ b/src/docs/components/Form/fields/FieldRichText/ComponentFieldRichText.vue @@ -0,0 +1,24 @@ + diff --git a/src/docs/components/Form/fields/FieldRichText/previews/PreviewFieldRichText.vue b/src/docs/components/Form/fields/FieldRichText/previews/PreviewFieldRichText.vue new file mode 100644 index 000000000..6d53eea81 --- /dev/null +++ b/src/docs/components/Form/fields/FieldRichText/previews/PreviewFieldRichText.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/docs/components/Form/fields/FieldRichText/readme.md b/src/docs/components/Form/fields/FieldRichText/readme.md new file mode 100644 index 000000000..eebe503c0 --- /dev/null +++ b/src/docs/components/Form/fields/FieldRichText/readme.md @@ -0,0 +1,16 @@ +## Props + +| Key | Description | +| --- | --- | +| `...` | Supports all props in [FieldBase](#/component/Form/fields/FieldBase) and [FieldRichTextarea](#/component/Form/fields/FieldRichTextarea). | +| `value` | The current value for this field. | +| `i18nFormattingLabel` | A localized string for the button to open formatting options. | +| `size` | Must be `oneline`. Default: `oneline` | + +## Events + +See [FieldBase](#/component/Form/fields/FieldBase). + +## Usage + +Use this component for a single-line rich text editor that supports bold, italics, and super-/sub-script. This field is designed to be used with submission titles. For most single-line text inputs, use [FieldText](#/component/Form/fields/FieldText). diff --git a/src/docs/components/Form/fields/FieldRichTextarea/readme.md b/src/docs/components/Form/fields/FieldRichTextarea/readme.md index 3bed76b27..7858e0ad7 100644 --- a/src/docs/components/Form/fields/FieldRichTextarea/readme.md +++ b/src/docs/components/Form/fields/FieldRichTextarea/readme.md @@ -17,13 +17,9 @@ See [FieldBase](#/component/Form/fields/FieldBase). ## Usage -Use this component to provide the user with a rich text editor that supports bold, italics, links and other HTML code. +Use this component to provide the user with a rich text editor that supports bold, italics, links and other HTML code. This component uses the TinyMCE editor. You can pass `toolbar`, `plugins` and `init` props to customize the editor. See the [TinyMCE documentation](https://www.tiny.cloud/docs/configure/integration-and-setup/). -This component uses the TinyMCE editor. You can pass `toolbar`, `plugins` and `init` props to customize the editor. See the [TinyMCE documentation](https://www.tiny.cloud/docs/configure/integration-and-setup/). - -The `size` of the input area will signal to the user how much information they should enter into the field. - -The default `size` is best for one to two large paragraphs. If you expect a user to enter more than that, consider using the large size when it fits appropriately within the the form where it appears. +The `size` of the input area will signal to the user how much information they should enter into the field. The default `size` is best for one to two large paragraphs. If you expect a user to enter more than that, consider using the large size when it fits appropriately within the the form where it appears. ## Image Uploads diff --git a/src/docs/components/Form/fields/FieldSelectIssue/readme.md b/src/docs/components/Form/fields/FieldSelectIssue/readme.md index bb8d8f234..58761a7a0 100644 --- a/src/docs/components/Form/fields/FieldSelectIssue/readme.md +++ b/src/docs/components/Form/fields/FieldSelectIssue/readme.md @@ -16,6 +16,6 @@ See [FieldBase](#/component/Form/fields/FieldBase). ## Usage -A special component for selecting an issue. +A special component for selecting an issue. When a publication is scheduled or published, this field will no longer show the selection options and will instead show a message indicating the issue it was scheduled or published in. -When a publication is scheduled or published, this field will no longer show the selection options and will instead show a message indicating the issue it was scheduled or published in. +In the example above, the "Assign to Issue" button will not work. This depends on the old JavaScript framework within the application. diff --git a/src/docs/components/Form/fields/FieldShowEnsuringLink/readme.md b/src/docs/components/Form/fields/FieldShowEnsuringLink/readme.md index e202cae1b..41b708149 100644 --- a/src/docs/components/Form/fields/FieldShowEnsuringLink/readme.md +++ b/src/docs/components/Form/fields/FieldShowEnsuringLink/readme.md @@ -4,7 +4,7 @@ | --- | --- | | `...` | Supports all props in [FieldBase](#/component/Form/fields/FieldBase). | | `value` | The current value for this field. | -| `message` | The message to display in the popup when the button in the confirmation message is clicked. **Note: the popup will not open in this demonstration.** | +| `message` | The message to display in the popup when the button in the confirmation message is clicked. | ## Events @@ -12,6 +12,4 @@ See [FieldBase](#/component/Form/fields/FieldBase). ## Usage -This component is a special field for the setting to show reviewers guidelines on how to ensure anonymous review. It displays a button to open the guidelines in a modal. - -The modal will _not_ open in the demonstration above. +This component is a special field for the setting to show reviewers the guidelines on how to ensure anonymous review. It displays a button to open the guidelines in a modal. diff --git a/src/docs/components/Form/fields/FieldText/ComponentFieldText.vue b/src/docs/components/Form/fields/FieldText/ComponentFieldText.vue index 64ea047be..89468cdd2 100644 --- a/src/docs/components/Form/fields/FieldText/ComponentFieldText.vue +++ b/src/docs/components/Form/fields/FieldText/ComponentFieldText.vue @@ -4,8 +4,6 @@ import PreviewFieldText from './previews/PreviewFieldText.vue'; import PreviewFieldTextTemplate from '!raw-loader!./previews/PreviewFieldText.vue'; import PreviewFieldTextSmall from './previews/PreviewFieldTextSmall.vue'; import PreviewFieldTextSmallTemplate from '!raw-loader!./previews/PreviewFieldTextSmall.vue'; -import PreviewFieldTextInline from './previews/PreviewFieldTextInline.vue'; -import PreviewFieldTextInlineTemplate from '!raw-loader!./previews/PreviewFieldTextInline.vue'; import PreviewFieldTextLarge from './previews/PreviewFieldTextLarge.vue'; import PreviewFieldTextLargeTemplate from '!raw-loader!./previews/PreviewFieldTextLarge.vue'; import PreviewFieldTextPrefix from './previews/PreviewFieldTextPrefix.vue'; @@ -47,11 +45,6 @@ export default { name: 'Editing Opt-in', template: this.extractTemplate(PreviewFieldTextOptIntoEditTemplate), }, - { - component: PreviewFieldTextInline, - name: 'Inline Label', - template: this.extractTemplate(PreviewFieldTextInlineTemplate), - }, ], }; }, diff --git a/src/docs/components/Form/fields/FieldText/previews/PreviewFieldTextInline.vue b/src/docs/components/Form/fields/FieldText/previews/PreviewFieldTextInline.vue deleted file mode 100644 index acaddd030..000000000 --- a/src/docs/components/Form/fields/FieldText/previews/PreviewFieldTextInline.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/src/docs/components/Form/fields/FieldText/readme.md b/src/docs/components/Form/fields/FieldText/readme.md index d7f1fad69..936cf82c8 100644 --- a/src/docs/components/Form/fields/FieldText/readme.md +++ b/src/docs/components/Form/fields/FieldText/readme.md @@ -9,7 +9,6 @@ | `optIntoEditLabel` | The label for the button added by `optIntoEdit`. | | `size` | One of `small`, `normal` or `large`. Default: `normal`. | | `prefix` | An optional prefix to show before the user's input. For example, a prefix of `http://publisher.com/` is used for the journal `path` field. | -| `isLabelInline` | When `true`, the label for this field will be shown inline instead of above the input field. See usage guidance below. | ## Events @@ -17,16 +16,6 @@ See [FieldBase](#/component/Form/fields/FieldBase). ## Usage -Use this component for entering a single line of text. +Use this component for entering a single line of text. The `size` of the input area will signal to the user how much information they should enter into the field. Choose a size that is sufficient to display the expected input. -The `size` of the input area will signal to the user how much information they should enter into the field. Choose a size that is sufficient to display the expected input. - - - -Do not use `isLabelInline` unless a designer has recommended this approach. Forms need to be designed carefully to support inline labels. - -### Prefix - -When using a `prefix`, be careful not to use a long prefix. There may not be enough room for the user to enter their information. If a prefix runs too long, it will be truncated so that the user can enter their information. - -A `prefix` can not be used with a localized value. Languages that read right-to-left (RTL) will not appear correctly. Only use a prefix in cases where the prefix will always read left-to-right, for example a domain name (http://test.com/) or a DOI prefix (10.1234/). +When using a `prefix`, be careful not to use a long prefix. There may not be enough room for the user to enter their information. If a prefix runs too long, it will be truncated so that the user can enter their information. A `prefix` can not be used with a localized value. Languages that read right-to-left (RTL) will not appear correctly. Only use a prefix in cases where the prefix will always read left-to-right, for example a domain name (http://test.com/) or a DOI prefix (10.1234/). diff --git a/src/docs/components/Form/fields/FieldUpload/readme.md b/src/docs/components/Form/fields/FieldUpload/readme.md index ec92de8d1..ea13ab138 100644 --- a/src/docs/components/Form/fields/FieldUpload/readme.md +++ b/src/docs/components/Form/fields/FieldUpload/readme.md @@ -14,8 +14,4 @@ See [FieldBase](#/component/Form/fields/FieldBase). Use this component when you want the user to upload a file. If you want them to upload an image, use the [FieldUploadImage](#/component/Form/fields/FieldUploadImage) component instead. -### Valid Upload URL - -You _must_ pass a `url` with the `options` prop. In most cases, the URL should correspond to your application's [API endpoint for temporary files](https://docs.pkp.sfu.ca/dev/api). - -If you do not use this endpoint, your endpoint should respond to `OPTIONS` and `POST` requests that [Dropzone.js](https://www.dropzonejs.com) makes to upload the file. The response should match that documented in the [API endpoint for temporary files](https://docs.pkp.sfu.ca/dev/api). \ No newline at end of file +You _must_ pass a `url` with the `options` prop. In most cases, the URL should correspond to the application's [API endpoint for temporary files](https://docs.pkp.sfu.ca/dev/api). If you do not use this endpoint, the endpoint should respond to `OPTIONS` and `POST` requests that [Dropzone.js](https://www.dropzonejs.com) makes to upload the file. The response should match what is documented in the [API endpoint for temporary files](https://docs.pkp.sfu.ca/dev/api). diff --git a/src/docs/components/Form/fields/FieldUploadImage/readme.md b/src/docs/components/Form/fields/FieldUploadImage/readme.md index 9ed869b87..320d60b70 100644 --- a/src/docs/components/Form/fields/FieldUploadImage/readme.md +++ b/src/docs/components/Form/fields/FieldUploadImage/readme.md @@ -13,6 +13,4 @@ See [FieldBase](#/component/Form/fields/FieldBase). ## Usage -Use this component when you want the user to upload an image. If you want to allow them to upload any file, use the [FieldUpload](#/component/Form/fields/FieldUpload) component instead. - -See the [FieldUpload](#/component/Form/fields/FieldUpload) component for guidance on passing an upload URL. +Use this component when you want the user to upload an image. If you want to allow them to upload any file, use the [FieldUpload](#/component/Form/fields/FieldUpload) component instead. See the [FieldUpload](#/component/Form/fields/FieldUpload) component for guidance on passing an upload URL. diff --git a/src/docs/components/Form/helpers/field-doi.js b/src/docs/components/Form/helpers/field-doi.js deleted file mode 100644 index 1c618e09d..000000000 --- a/src/docs/components/Form/helpers/field-doi.js +++ /dev/null @@ -1,20 +0,0 @@ -export default { - name: 'doi', - component: 'field-pub-id', - label: 'DOI', - value: '', - contextInitials: 'PKP', - issueNumber: '2', - issueVolume: '14', - pattern: '%j.v%vi%i.%a', - prefix: '10.1234', - separator: '/', - submissionId: 21, - year: '2019', - assignIdLabel: 'Assign DOI', - clearIdLabel: 'Clear DOI', - missingPartsLabel: - 'You can not generate a DOI because one or more parts of the DOI pattern are missing data. You may need to assign the publication to an issue, set a publisher ID or enter page numbers.', - missingIssueLabel: - 'You can not generate a DOI until this publication has been assigned to an issue.', -}; diff --git a/src/docs/components/Form/helpers/field-options-categories.js b/src/docs/components/Form/helpers/field-options-categories.js index 6f1c28726..5d34cff09 100644 --- a/src/docs/components/Form/helpers/field-options-categories.js +++ b/src/docs/components/Form/helpers/field-options-categories.js @@ -1,31 +1,17 @@ +import categories from '../../../data/categories'; + export default { name: 'categories', component: 'field-options', label: 'Categories', description: 'Select only the categories that are appropriate for your submission.', - value: '', - options: [ - { - value: 4, - label: 'Biology', - }, - { - value: 1, - label: 'Health Sciences', - }, - { - value: 2, - label: 'Health Sciences > Tropical Medicine', - }, - { - value: 3, - label: 'Health Sciences > Radiology', - }, - { - value: 5, - label: '...', - }, - ], + value: [], + options: Object.keys(categories).map((id) => { + return { + value: id, + label: categories[id], + }; + }), groupId: 'default', }; diff --git a/src/docs/components/Form/helpers/field-radio-input.js b/src/docs/components/Form/helpers/field-radio-input.js index ce432e97f..02c5aa482 100644 --- a/src/docs/components/Form/helpers/field-radio-input.js +++ b/src/docs/components/Form/helpers/field-radio-input.js @@ -5,15 +5,15 @@ export default { value: 'something', options: [ { - value: 'one', - label: 'One', + value: 'yes', + label: 'Yes', }, { - value: 'two', - label: 'Two', + value: 'no', + label: 'No', }, { - label: 'Any', + label: 'Other', isInput: true, }, ], diff --git a/src/docs/components/Form/helpers/field-rich-textarea-abstract.js b/src/docs/components/Form/helpers/field-rich-textarea-abstract.js index b06234a84..415a1489a 100644 --- a/src/docs/components/Form/helpers/field-rich-textarea-abstract.js +++ b/src/docs/components/Form/helpers/field-rich-textarea-abstract.js @@ -7,8 +7,7 @@ export default { plugins: 'paste,link,noneditable', toolbar: 'bold italic superscript subscript | link', value: { - en: - '

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

', + en: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

', fr_CA: '', ar: '', }, diff --git a/src/docs/components/Form/helpers/field-rich-textarea-signature.js b/src/docs/components/Form/helpers/field-rich-textarea-signature.js index 4c37373b2..fe8132927 100644 --- a/src/docs/components/Form/helpers/field-rich-textarea-signature.js +++ b/src/docs/components/Form/helpers/field-rich-textarea-signature.js @@ -6,8 +6,7 @@ export default { fr_CA: 'Signature', }, description: { - en: - 'Add a personal signature you would like to be included with any emails we send on your behalf.', + en: 'Add a personal signature you would like to be included with any emails we send on your behalf.', fr_CA: 'Ajoutez une signature personnelle que vous souhaitez inclure dans les courriels que nous envoyons en votre nom.', }, diff --git a/src/docs/components/Form/helpers/field-upload-image-logo.js b/src/docs/components/Form/helpers/field-upload-image-logo.js index e7f34dda1..b39e40746 100644 --- a/src/docs/components/Form/helpers/field-upload-image-logo.js +++ b/src/docs/components/Form/helpers/field-upload-image-logo.js @@ -5,7 +5,7 @@ export default { options: { url: 'https://httpbin.org/post', }, - baseUrl: 'http://localhost:8000/public/journals/1/', + baseUrl: 'https://httbin.org/public/journals/1/', altTextDescription: 'Describe this image for visitors viewing the site in a text-only browser or with assistive devices. Example: "Our editor speaking at the PKP conference."', altTextLabel: 'Alt Text', diff --git a/src/docs/components/Form/helpers/field-urn.js b/src/docs/components/Form/helpers/field-urn.js new file mode 100644 index 000000000..a9db55f76 --- /dev/null +++ b/src/docs/components/Form/helpers/field-urn.js @@ -0,0 +1,20 @@ +export default { + name: 'urn', + component: 'field-pub-id', + label: 'URN', + value: '', + contextInitials: 'PKP', + issueNumber: '2', + issueVolume: '14', + pattern: 'v%vi%i.%a', + prefix: 'urn:pkp:1234', + separator: '-', + submissionId: 21, + year: '2019', + assignIdLabel: 'Assign URN', + clearIdLabel: 'Clear URN', + missingPartsLabel: + 'You can not generate a URN because one or more parts of the URN pattern are missing data. You may need to assign the publication to an issue, set a publisher ID or enter page numbers.', + missingIssueLabel: + 'You can not generate a URN until this publication has been assigned to an issue.', +}; diff --git a/src/docs/components/Form/helpers/form-change-submission.js b/src/docs/components/Form/helpers/form-change-submission.js new file mode 100644 index 000000000..26387cdb2 --- /dev/null +++ b/src/docs/components/Form/helpers/form-change-submission.js @@ -0,0 +1,63 @@ +import Form from './form'; + +export default { + ...Form, + id: 'confirm', + action: 'emit', + fields: [ + { + name: 'section', + component: 'field-options', + label: 'Section', + value: 1, + type: 'radio', + options: [ + { + value: 1, + label: 'Articles', + }, + { + value: 2, + label: 'Reviews', + }, + { + value: 3, + label: 'Editorials', + }, + ], + groupId: 'default', + }, + { + name: 'language', + component: 'field-options', + label: 'Language', + value: 'en', + type: 'radio', + options: [ + { + value: 'en', + label: 'English', + }, + { + value: 'fr_CA', + label: 'French (Canadian)', + }, + ], + groupId: 'default', + }, + ], + groups: [ + { + id: 'default', + pageId: 'default', + }, + ], + pages: [ + { + id: 'default', + submitButton: { + label: 'Save', + }, + }, + ], +}; diff --git a/src/docs/components/Form/helpers/form-confirm.js b/src/docs/components/Form/helpers/form-confirm.js index 750f1a5ac..6ba513b06 100644 --- a/src/docs/components/Form/helpers/form-confirm.js +++ b/src/docs/components/Form/helpers/form-confirm.js @@ -1,12 +1,11 @@ import Form from './form'; import FieldOptionsCopyright from './field-options-copyright'; -import FieldOptionsSubmissionAgreement from './field-options-submission-agreement'; export default { ...Form, id: 'confirm', action: '/example/submit/1', - fields: [{...FieldOptionsCopyright}, {...FieldOptionsSubmissionAgreement}], + fields: [{...FieldOptionsCopyright}], groups: [ { id: 'default', diff --git a/src/docs/components/Form/helpers/form-for-the-editors.js b/src/docs/components/Form/helpers/form-for-the-editors.js index bec49d9fc..c906a2290 100644 --- a/src/docs/components/Form/helpers/form-for-the-editors.js +++ b/src/docs/components/Form/helpers/form-for-the-editors.js @@ -1,5 +1,4 @@ import Form from './form'; -import FieldKeywords from './field-controlled-vocab-keywords'; import FieldCategories from './field-options-categories'; import FieldRichTextareaAbstract from './field-rich-textarea-abstract'; @@ -8,7 +7,6 @@ export default { id: 'forTheEditors', action: '/example/publications/1', fields: [ - {...FieldKeywords, groupId: 'default'}, {...FieldCategories, groupId: 'default'}, { ...FieldRichTextareaAbstract, diff --git a/src/docs/components/Form/readme.md b/src/docs/components/Form/readme.md index 74f57e5cd..cdaef7061 100644 --- a/src/docs/components/Form/readme.md +++ b/src/docs/components/Form/readme.md @@ -27,16 +27,12 @@ | 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. | +| `notify` | When an error is encountered during form submission. See [Notify](#/utilities/Notify). | ## Usage -Use this component to display a form. - -## Pass props - -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. +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 ``` -## Multi-page Forms - -Use a multi-page form when you want to guide the user through a multi-step process that ends in a single action. Reviewer assignment is an example of a multi-step process: +Learn more about [server-side form components](https://docs.pkp.sfu.ca/dev/documentation/en/frontend-forms). -1. Select a reviewer -2. Select a review type and enter a deadline -3. Prepare the notification email - -We want to divide the action into smaller steps so that we don't overwhelm the user, but we shouldn't take any action until all steps are complete. - -Do _not_ use a multi-page form for all related forms, such as those on the settings page. A user may wish to only edit one setting and that change can be saved immediately without depending on further action in related forms. +## Multi-page Forms -If you want to indicate that forms are related, without requiring them to be submitted together, use [tabs](#/component/Tab). +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 -A `Form` expects to receive a URL to which it can submit a `PUT` or `POST` request to the application's API. Forms can handle the following responses from the API automatically: +The `action` prop of most `
` 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 one of the fields. In this case, a JSON object will be returned with each invalid field as a key and an array of errors for that field. +- A `400` response when a validation error occurs with a JSON object describing the validation errors. The `` 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. diff --git a/src/docs/components/Header/readme.md b/src/docs/components/Header/readme.md index 5196df12c..37b39eb9d 100644 --- a/src/docs/components/Header/readme.md +++ b/src/docs/components/Header/readme.md @@ -10,4 +10,4 @@ This component does not emit any events. ## Usage -The `Header` component defines a consistent UI for providing a group of related components with a title, actions which interact with those components, and a loading spinner when requests are being processed. \ No newline at end of file +Use this component to display a title alongside actions related to that title, such as a list of buttons or a progress spinner. diff --git a/src/docs/components/Icon/readme.md b/src/docs/components/Icon/readme.md index 74418e245..1b67a5bf4 100644 --- a/src/docs/components/Icon/readme.md +++ b/src/docs/components/Icon/readme.md @@ -11,15 +11,15 @@ This component does not emit any events ## Usage -Use this component to display a [FontAwesome v4.7](https://fontawesome.com/v4.7.0) icon. See [all icons](https://fontawesome.com/v4.7.0/icons/). +Use this component to display an icon. Use icons when you want to visually connect two parts of the UI that are not otherwise connected. For example, the [ListPanel](#/component/ListPanel) filters use an icon to link the button with the panel it controls. -Use icons when you want to visually connect two parts of the UI that are not otherwise connected. For example, the [ListPanel](#/component/ListPanel) filters use an icon to link the button with the panel it controls. +Use any [FontAwesome v4 icon](https://fontawesome.com/v4/icons/). -### Be careful when using icon-only labels +## Be careful when using icon-only labels You may want to use icon-only labels when fitting a lot of information into a small space. Such icon-based display can be useful for presenting dense information, but these patterns tend to favor experienced users over novice ones. Use with caution. -### Avoid decorative icons +## Avoid decorative icons When adding buttons or other controls, do not add an icon when the text label sufficiently describes the action. diff --git a/src/docs/components/List/readme.md b/src/docs/components/List/readme.md index aea23775c..919968f78 100644 --- a/src/docs/components/List/readme.md +++ b/src/docs/components/List/readme.md @@ -8,42 +8,4 @@ This component does not emit any events. ## Usage -Use this component to display simple lists of content that should be grouped together. - -Lists use [component slots](https://vuejs.org/v2/guide/components.html#Content-Distribution-with-Slots) which allow you to compose lists directly in the template. Wrap any content in a `` and `` like this: - -```html - - - Anything goes here. - - -``` - -You can also use other components inside a ``. - -```html - - - Submit - - -``` - -Pass a short piece of information to the `value` slot and it will appear in a consistent style. - -```html - - - - Number of active discussions in this journal. - - -``` - - -## Usage Recommendations - -If one `value` slot includes an icon, all `value` slots should include icons. The user should be able to scan from top to bottom with all text aligned vertically on the left. +Use this component to display simple lists of content that should be grouped together. If one list item uses an icon in the `value` slot, all items should use an icon in the `value` slot. The user should be able to scan from top to bottom with all text aligned vertically on the left. diff --git a/src/docs/components/ListPanel/ComponentAnnouncementsListPanel.vue b/src/docs/components/ListPanel/ComponentAnnouncementsListPanel.vue index b6296ec2b..857234604 100644 --- a/src/docs/components/ListPanel/ComponentAnnouncementsListPanel.vue +++ b/src/docs/components/ListPanel/ComponentAnnouncementsListPanel.vue @@ -2,12 +2,14 @@ import Component from '@/docs/Component.vue'; import PreviewAnnouncementsListPanel from './previews/PreviewAnnouncementsListPanel.vue'; import PreviewAnnouncementsListPanelTemplate from '!raw-loader!./previews/PreviewAnnouncementsListPanel.vue'; +import readme from '!raw-loader!./readme-announcements.md'; export default { extends: Component, data() { return { name: 'AnnouncementsListPanel', + readme: readme, examples: [ { component: PreviewAnnouncementsListPanel, diff --git a/src/docs/components/ListPanel/ComponentCatalogListPanel.vue b/src/docs/components/ListPanel/ComponentCatalogListPanel.vue index c6e06c91f..6d41e9dd0 100644 --- a/src/docs/components/ListPanel/ComponentCatalogListPanel.vue +++ b/src/docs/components/ListPanel/ComponentCatalogListPanel.vue @@ -2,12 +2,14 @@ import Component from '@/docs/Component.vue'; import PreviewCatalogListPanel from './previews/PreviewCatalogListPanel.vue'; import PreviewCatalogListPanelTemplate from '!raw-loader!./previews/PreviewCatalogListPanel.vue'; +import readme from '!raw-loader!./readme-catalog.md'; export default { extends: Component, data() { return { name: 'CatalogListPanel', + readme: readme, examples: [ { component: PreviewCatalogListPanel, diff --git a/src/docs/components/ListPanel/ComponentInstitutionsListPanel.vue b/src/docs/components/ListPanel/ComponentInstitutionsListPanel.vue index 3b9b62ff1..3871ca95a 100644 --- a/src/docs/components/ListPanel/ComponentInstitutionsListPanel.vue +++ b/src/docs/components/ListPanel/ComponentInstitutionsListPanel.vue @@ -2,12 +2,14 @@ import Component from '@/docs/Component.vue'; import PreviewInstitutionsListPanel from './previews/PreviewInstitutionsListPanel.vue'; import PreviewInstitutionsListPanelTemplate from '!raw-loader!./previews/PreviewInstitutionsListPanel.vue'; +import readme from '!raw-loader!./readme-institutions.md'; export default { extends: Component, data() { return { name: 'InstitutionsListPanel', + readme: readme, examples: [ { component: PreviewInstitutionsListPanel, diff --git a/src/docs/components/ListPanel/ComponentSelectReviewerListPanel.vue b/src/docs/components/ListPanel/ComponentSelectReviewerListPanel.vue index 2e4807a8b..dbe198b6f 100644 --- a/src/docs/components/ListPanel/ComponentSelectReviewerListPanel.vue +++ b/src/docs/components/ListPanel/ComponentSelectReviewerListPanel.vue @@ -2,12 +2,14 @@ import Component from '@/docs/Component.vue'; import PreviewSelectReviewerListPanel from './previews/PreviewSelectReviewerListPanel.vue'; import PreviewSelectReviewerListPanelTemplate from '!raw-loader!./previews/PreviewSelectReviewerListPanel.vue'; +import readme from '!raw-loader!./readme-select-reviewer.md'; export default { extends: Component, data() { return { name: 'SelectReviewerListPanel', + readme: readme, examples: [ { component: PreviewSelectReviewerListPanel, diff --git a/src/docs/components/ListPanel/ComponentSubmissionFilesListPanel.vue b/src/docs/components/ListPanel/ComponentSubmissionFilesListPanel.vue index d78088433..5c47bc727 100644 --- a/src/docs/components/ListPanel/ComponentSubmissionFilesListPanel.vue +++ b/src/docs/components/ListPanel/ComponentSubmissionFilesListPanel.vue @@ -2,12 +2,14 @@ import Component from '@/docs/Component.vue'; import PreviewSubmissionFilesListPanel from './previews/PreviewSubmissionFilesListPanel.vue'; import PreviewSubmissionFilesListPanelTemplate from '!raw-loader!./previews/PreviewSubmissionFilesListPanel.vue'; +import readme from '!raw-loader!./readme-submission-files.md'; export default { extends: Component, data() { return { name: 'SubmissionFilesListPanel', + readme: readme, examples: [ { component: PreviewSubmissionFilesListPanel, diff --git a/src/docs/components/ListPanel/ComponentSubmissionsListPanel.vue b/src/docs/components/ListPanel/ComponentSubmissionsListPanel.vue index 0c6947fb4..d73cb6e66 100644 --- a/src/docs/components/ListPanel/ComponentSubmissionsListPanel.vue +++ b/src/docs/components/ListPanel/ComponentSubmissionsListPanel.vue @@ -2,12 +2,14 @@ import Component from '@/docs/Component.vue'; import PreviewSubmissionsListPanel from './previews/PreviewSubmissionsListPanel.vue'; import PreviewSubmissionsListPanelTemplate from '!raw-loader!./previews/PreviewSubmissionsListPanel.vue'; +import readme from '!raw-loader!./readme-submissions.md'; export default { extends: Component, data() { return { name: 'SubmissionsListPanel', + readme: readme, examples: [ { component: PreviewSubmissionsListPanel, diff --git a/src/docs/components/ListPanel/previews/PreviewAnnouncementsListPanel.vue b/src/docs/components/ListPanel/previews/PreviewAnnouncementsListPanel.vue index 7779ba873..e61b6e6e1 100644 --- a/src/docs/components/ListPanel/previews/PreviewAnnouncementsListPanel.vue +++ b/src/docs/components/ListPanel/previews/PreviewAnnouncementsListPanel.vue @@ -42,12 +42,10 @@ export default { datePosted: '2019-12-12', id: 2, title: { - en: - 'Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at turpis massa tincidunt dui ut ornare lectus', + en: 'Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at turpis massa tincidunt dui ut ornare lectus', fr_CA: 'Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at turpis massa tincidunt dui ut ornare lectus', - ar: - 'Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at turpis massa tincidunt dui ut ornare lectus', + ar: 'Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at turpis massa tincidunt dui ut ornare lectus', }, }, { @@ -67,12 +65,10 @@ export default { datePosted: '2019-09-21', id: 4, title: { - en: - 'Morbi tincidunt ornare massa eget egestas purus viverra accumsan', + en: 'Morbi tincidunt ornare massa eget egestas purus viverra accumsan', fr_CA: 'Morbi tincidunt ornare massa eget egestas purus viverra accumsan', - ar: - 'Morbi tincidunt ornare massa eget egestas purus viverra accumsan', + ar: 'Morbi tincidunt ornare massa eget egestas purus viverra accumsan', }, }, { diff --git a/src/docs/components/ListPanel/previews/PreviewInstitutionsListPanel.vue b/src/docs/components/ListPanel/previews/PreviewInstitutionsListPanel.vue index 9d3e2652a..7d3679dcb 100644 --- a/src/docs/components/ListPanel/previews/PreviewInstitutionsListPanel.vue +++ b/src/docs/components/ListPanel/previews/PreviewInstitutionsListPanel.vue @@ -10,7 +10,6 @@ :items="items" :itemsMax="itemsMax" title="Institutions" - urlBase="https://example.com/institution/view/__id__" /> diff --git a/src/docs/components/SubmissionWizardPage/readme.md b/src/docs/components/SubmissionWizardPage/readme.md index c64e89bc1..9cad880f5 100644 --- a/src/docs/components/SubmissionWizardPage/readme.md +++ b/src/docs/components/SubmissionWizardPage/readme.md @@ -1,15 +1,161 @@ ## Data +This is a root component. Learn about [page hydration](#/pages/pages). + | Key | Description | | --- | --- | -| `...` |... | +| `...` | All the data of [Page](#/component/Page). | +| `autosavesKeyBase` | A unique string. See autosave mixin below. Default: `'submitAutosaves'` | +| `categories` | **Required** A key/value map of category ids and category names. Example: `{12: "Social Sciences > Sociology"}`. Not required if there is no categories field in the submission wizard. Default: `{}` | +| `currentStepId` | Default: `''` | +| `errors` | A key/value map of submission validation errors. Default: `{}` | +| `i18nConfirmSubmit` | **Required** A localized string for the confirmation message before submitting. | +| `i18nDiscardChanges` | **Required** A localized string for the button to discard changes when unsaved changes are found. | +| `i18nDisconnected` | **Required** A localized string to show when there is no connection to the server. | +| `i18nLastAutosaved` | **Required** A localized string describing when the last autosave occured. | +| `i18nPageTitle` | **Required** A localized string describing the page with the step. This is used in the page `` so that the browser tab and browser history provide useful information. Example: `Make a Submission: {$step}`. | +| `i18nSubmit` | **Required** A localized string for the submit button. | +| `i18nTitleSeparator` | **Required** A localized string to use as a separator in the page title. See `i18nPageTitle`. | +| `i18nUnableToSave` | **Required** A localized string to show when an error occurred during an autosave, such as a failure to connect to the server. | +| `i18nUnsavedChanges` | **Required** A localized string for the title of the dialog that appears when unsaved changes are found in local storage. | +| `i18nUnsavedChangesMessage` | **Required** A localized string for the message of the dialog that appears when unsaved changes are found in local storage. | +| `isValidating` | Is the submission validation request pending? Default: `false` | +| `lastAutosavedMessage` | A localized string describing when the last save was completed. Uses `i18nLastAutosaved`. Default: `''` | +| `publication` | **Required** The `Publication` being edited. | +| `publicationApiUrl` | **Required** The URL to the REST API endpoint for this `Publication`. | +| `reconfigurePublicationProps` | An array of `Publication` property names which can be edited by the form to reconfigure the submission. See "Reconfigure Submission" below. Default: `[]` | +| `reconfigureSubmissionProps` | An array of `Submission` property names which can be edited by the form to reconfigure the submission. See "Reconfigure Submission" below. Default: `[]` | +| `staleForms` | An array of ids of forms that have changed since the last save. Default: `[]` | +| `startedSteps` | An array of ids of steps that have been started. Default: `[]` | +| `steps` | **Required** An array of steps. See "Steps and Sections" below. | +| `submission` | The `Submission` being edited. Default: `{}` | +| `submissionApiUrl` | **Required** The URL to the REST API endpoint for this submission. | +| `submissionSavedUrl` | **Required** The URL to the page to show when a submission is saved for later. | +| `submissionWizardUrl` | **Required** The URL to the submission wizard page. | +| `submitApiUrl` | **Required** The URL to the REST API endpoint to submit this submission. | + +## Mixins + +| Name | Description | +| --- | --- | +| `ajaxError` | Displays a [Dialog](#/mixins/dialog) when an unexpected response is encountered. | +| [autosave](#/pages/autosave) | Autosave the user's progress. | +| [dialog](#/mixins/dialog) | Show confirmation prompts. | +| [localizeMoment](#/pages/localizeMoment) | Localize the last saved message. | +| `localizeSubmission` | Localize submission properties. | +| [localStorage](#/pages/localStorage) | Save ther user's progress to local storage when connection lost. | ## Usage -The `SubmissionWizardPage` extends the [`Container`](/#/pages/container) app. Use this app to show the submission wizard. +<div class="pkpNotification pkpNotification--warning"> +This component preview is only partially functional. The component uses the server to save and sync data, so information in the review step is not updated based on input from earlier steps. Also, it is not possible to add contributors, upload files, or make use of the autosave features. +</div> + +The `<SubmissionWizardPage>` extends the [Page](#/component/Page) component. Use this page to show the submission wizard. This makes use of the [SubmissionFilesListPanel](#/component/ListPanel/components/SubmissionFilesListPanel) and `<ContributorsListPanel>`. Add props for these components to the `components` prop. + +```json +{ + "components": { + "contributors": {...}, + "submissionFiles": {...} + } +} +``` + +## Steps, Sections and Review + +The `steps` property is an array of steps. Each step includes an id, name, an array of sections, and information about how these sections should be displayed in the review step. + +```json +[ + { + "id": "details", + "name": "Details", + "reviewName": "Details You Entered", + "reviewTemplate": "/submission/review-details.tpl", + "sections": [ + ... + ] + } +] +``` + +The `reviewTemplate` points to a [template file](https://docs.pkp.sfu.ca/dev/documentation/en/frontend-pages#smarty) that can display the entered information on the review stage. This is not used in the browser, but is part of how the steps are defined for the server-side templates. Server-side templating is used so that plugins can make use of [template overrides](https://docs.pkp.sfu.ca/dev/plugin-guide/en/templates) to customize the wizard. + +All sections include a name and description, and must be one of the recognized types. The `contributors` type will display a `<ContributorsListPanel>`. It will pull the component's props from the `components.contributors` and `publication` props of this page. + +```json +{ + "id": "contributors", + "name": "Contributors", + "description": "Please add all the contributors to this submission.", + "type": "contributors" // APP\pages\submission\SubmissionHandler::SECTION_TYPE_CONTRIBUTORS +} +``` + +The `files` type will display a [SubmissionFilesListPanel](#/component/ListPanel/components/SubmissionFilesListPanel). It will pull the component's props from the `components.submissionFiles` props of this page. + +```json +{ + "id": "files", + "name": "Upload Files", + "description": "Please upload all files our editorial staff need to evaluate your submission.", + "type": "files" // APP\pages\submission\SubmissionHandler::SECTION_TYPE_FILES +} +``` + +The `form` type will display a [Form](#/component/Form) and autosave all changes to it. The form's props should be passed with the section. + +```json +{ + "id": "details", + "name": "Details", + "description": "Please enter the title, abstract and other information about this submission.", + "type": "form", // APP\pages\submission\SubmissionHandler::SECTION_TYPE_FORM + "form": { + ... + } +} +``` + +The `confirm` type is a special type that is used in the review step to prevent the submission from being submitted before the author has confirmed the copyright notice. When used this way, the section id should be `confirmSubmission`. It will display a [Form](#/componentForm) that will not be autosaved. The form's props should be passed with the section. + +```json +{ + "id": "confirmSubmission", + "name": "Copyright Notice", + "description": "Please agree to the copyright notice before submitting.", + "type": "form", // APP\pages\submission\SubmissionHandler::SECTION_TYPE_CONFIRM + "form": { + ... + } +} +``` + +The `review` type is a special type to show a review of the data. No configuration is needed, but if a section is added it will be displayed after the review. + +```json +{ + "id": "review", + "name": "Review", + "description": "Please review this information before completing your submission.", + "type": "review" // APP\pages\submission\SubmissionHandler::SECTION_TYPE_REVIEW +} +``` + +Other types can be created and added to the wizard by modifying the `wizard.tpl` template file or using the `Template::SubmissionWizard::Section` and `Template::SubmissionWizard::Section` hooks. + +## Reconfigure Submission + +Some submission properties like the language or section can only be changed through a special form. This appears at the top of the wizard, where it says something like "Submitting to the Articles section in English. These properties are changed separately from the rest of the wizard because they may effect the configuration of the wizard itself. Once the form is saved, the wizard is reloaded with the correct configuration for that section and language. -## Template +To map the values of this form to the correct API endpoint, you must set the `reconfigurePublicationProps` and `reconfigureSubmissionProps`. For example, the `sectionId` is a property of the `Publication`, but the `locale` is a property of the `Submission`. To ensure that each property is saved to the correct API endpoint, pass the following data to `<SubmissionWizardPage>`. -The `SubmissionWizardPage` app is a template-less component. You must write the template in Smarty on the server-side and it will be compiled at run time. +```json +{ + "reconfigurePublicationProps": ["sectionId"], + "reconfigureSubmissionProps": ["locale"] +} +``` -The example here provides a sample template. +If more fields are added to the form, the properties must be added to these two props. diff --git a/src/docs/components/Table/readme.md b/src/docs/components/Table/readme.md index 17e3696a0..fb15dc3db 100644 --- a/src/docs/components/Table/readme.md +++ b/src/docs/components/Table/readme.md @@ -3,10 +3,10 @@ | Key | Description | | --- | --- | | `columns` | An array of configuration objects for each column. | -| `describedBy` | The id of one or more HTML elements that describe the table, to be used in an <code>aria-describedby</code> attribute. This prop is not required unless additional information about the table is available and not included in the <code>description</code> prop. | +| `describedBy` | Optional. See "External Labelling" below. | | `description` | An optional description of the table. | -| `label` | A name for the table. If no <code>label</code> is provided, you must make use of the <code>labelledBy</code> prop. | -| `labelledBy` | The id of one or more HTML elements that name the table, to be used in an <code>aria-labelledby</code> attribute. This prop should not be used if a <code>label</code> is provided. | +| `label` | A name for the table. If no <code>label</code> is provided, you must use the <code>labelledBy</code> prop. See "External Labelling" below. | +| `labelledBy` | Optional. See "External Labelling" below. | | `orderBy` | The name of the column that the rows are ordered by. | | `orderDirection` | The direction that rows are ordered by as a boolean. | | `rows` | The items to display in the table. | @@ -19,9 +19,7 @@ ## Usage -Use the `Table` component to provide tabular data when the user will interact with the data, such as sorting, searching, filtering or editing the rows. - -Do not use this component when the user will not interact with the table in these ways. You can write a `<table>` in plain HTML code for better performance. Use the class name, `pkpTable`, to apply consistent styles to your table. +Use the `Table` component to provide tabular data when the user will interact with the data, such as sorting, searching, filtering or editing the rows. Do not use this component when the user will not interact with the table in these ways. You can write a `<table>` in plain HTML code for better performance. Use the class name, `pkpTable`, to apply consistent styles to your table. ## Datagrid @@ -31,13 +29,11 @@ When using this component with a custom row slot, pay special attention to the u All interactions in the table rows, including buttons or fields to edit a row, must be accessible by keyboard. Navigation with the up, down, left and right arrows should not be impacted by any interactive elements in a row. -If you extend this component with your own template, be sure to include accessible labeling. When appropriate, use the `scope` and `aria-sort` attributes in the header cells. - ## External Labelling You may wish to label the table with text that is not part of the table itself. In this case, the `labelledBy` prop must be used to link the table to its label. If you have an additional description, you must use the `describedBy` prop to link this description to the table. -See the [Labelled By](#/component/Table/LabelledBy) example. +See the [Labelled By](#/component/Table/with-labelledby) example. ## Slots diff --git a/src/docs/components/Tabs/readme.md b/src/docs/components/Tabs/readme.md index 220cbbd4e..061ad335f 100644 --- a/src/docs/components/Tabs/readme.md +++ b/src/docs/components/Tabs/readme.md @@ -1,20 +1,30 @@ ## Props +Props for the outer `<Tabs>` element. + | Key | Description | | --- | --- | | `defaultTab` | Select one of the tabs by default. Pass the tab's `id` prop. | -| `isSideTabs` | Displays the tabs on the side with content on the right when `true`. | -| `label` | Sets an `aria-label` for the tabs. See the [Accessible label](#accessible-label) section below. | -| `badge` | Adds a [Badge](#/component/Badge) component beside the icon or text in the tab. | +| `isSideTabs` | Displays the tabs on the side with content beside it when `true`. | +| `label` | Sets an `aria-label` for the tabs. Read the [accessible label](#accessible-label) section below. | | `trackHistory` | When `true`, changes to the current tab will modify the browser history so that the back button can be used. | +Props for each `<Tab>` element. + +| Key | Description | +| --- | --- | +| `badge` | Adds a [Badge](#/component/Badge) component beside the icon or text in the tab. | +| `icon` | Adds an [Icon](#/component/Icon) component beside the text in the tab. | +| `id` | A unique string for this tab. Required. | +| `label` | A text label for this tab. | + ## Events This component does not emit any events. ## Usage -Use this component to display content in tabs. +Use this component to display content in tabs. This component implements the `role="tablist"` specification. Once the user has navigated to the tab headings, they should be able to change tabs by using the <kbd>←</kbd> and <kbd>→</kbd> keys. Pressing the <kbd>HOME</kbd> and <kbd>END</kbd> keys should take them to the first and last tab. ## Accessible label @@ -27,7 +37,15 @@ Pass a `label` to the `Tabs` component to improve the experience for assistive t </tabs> ``` -A `label` is not needed when the tabs immediately follow a heading which describes the tabs. Otherwise a `label` should be used. +A `label` is not needed when the tabs immediately follow a heading which describes the tabs. + +```html +<h1>Website Settings</h1> +<tabs> + <tab>...</tab> + <tab>...</tab> +</tabs> +``` ## Open tab programmatically @@ -55,9 +73,7 @@ If the `hello` tab is currently visible, firing `pkp.eventBus.$emit('open-tab', ## Icon-only Tab Buttons -Avoid using icon-only tabs except in rare cases. A text label is almost always easier to understand. - -You must still pass a `label` to a `Tab` when using the `icon` property. It will be read out by assistive technology. +Avoid using icon-only tabs except in rare cases. A text label is almost always easier to understand. You must still pass a `label` to a `Tab` when using the `icon` property. It will be read out by assistive technology. ## Deep Nesting diff --git a/src/docs/components/Tooltip/readme.md b/src/docs/components/Tooltip/readme.md index 45dc4353a..bbdcf1f66 100644 --- a/src/docs/components/Tooltip/readme.md +++ b/src/docs/components/Tooltip/readme.md @@ -11,8 +11,4 @@ This component does not emit any events. ## Usage -Use this component to add short tips that appear above a button. - -Use the tooltip when you want to provide short advice on how to complete a task. If the advice can not be fit in 25-30 words, consider using a [HelpButton](#/component/HelpButton) instead. - -Do _not_ use the tooltip for information that the user requires to complete a task. Most users will not interact with the tooltip, so you should not rely on them reading the information. +Use the tooltip when you want to provide short advice on how to complete a task. If the advice can not be fit in 25-30 words, consider using a [HelpButton](#/component/HelpButton) instead. Do _not_ use the tooltip for information that the user requires to complete a task. Most users will not interact with the tooltip, so you should not rely on them reading the information. diff --git a/src/docs/components/WorkflowPage/previews/PreviewWorkflowPage.vue b/src/docs/components/WorkflowPage/previews/PreviewWorkflowPage.vue index a8845dbb4..c7d4db1ab 100644 --- a/src/docs/components/WorkflowPage/previews/PreviewWorkflowPage.vue +++ b/src/docs/components/WorkflowPage/previews/PreviewWorkflowPage.vue @@ -61,6 +61,9 @@ <tab id="workflow" label="Workflow"> <notification type="warning"> This component preview does not include the full workflow. + <pkp-button @click="showTab('publication')"> + Show Publication + </pkp-button> </notification> </tab> <tab id="publication" label="Publication"> @@ -225,10 +228,9 @@ <tab id="metadata" label="Metadata"> <pkp-form v-bind="components.metadata" @set="set" /> </tab> - <tab v-if="supportsReferences" id="citations" label="Citations"> + <tab v-if="supportsReferences" id="citations" label="References"> <pkp-form v-bind="components.citations" @set="set" /> </tab> - <tab id="identifiers" label="Identifiers">... DOIs ...</tab> <tab id="galleys" label="Galleys"> <div id="representations-grid" ref="representations"> <spinner></spinner> @@ -325,22 +327,33 @@ export default { canEditPublication: true, components: { contributors: { + canEditPublication: true, id: 'contributors', - items: [...authors], - title: 'Contributors', i18nAddContributor: 'Add Contributor', - publicationApiUrl: - 'http://localhost:8088/index.php/ts/api/v1/submissions/16/publications', i18nConfirmDelete: - 'Are you sure you want to remove {$name} as a contributor? This action can not be undone.', - i18nDeleteContributor: 'Delete', + 'Are you sure you want to remove this contributor?', + i18nDeleteContributor: 'Delete Contributor', i18nEditContributor: 'Edit', - canEditPublication: true, + i18nSetPrimaryContact: 'Set Primary Contact', + i18nPrimaryContact: 'Primary Contact', + i18nContributors: 'Contributors', + i18nSaveOrder: 'Save Order', + i18nPreview: 'Preview', + i18nPreviewDescription: + 'Contributors to this publication will be identified in this journal in the following formats.', + i18nDisplay: 'Display', + i18nFormat: 'Format', + i18nAbbreviated: 'Abbreviated', + i18nPublicationLists: 'Publication Lists', + i18nFull: 'Full', + items: [...authors], + publicationApiUrlFormat: '', + title: 'Contributors', form: { id: 'contributor', method: 'POST', action: - 'http://localhost:8088/index.php/ts/api/v1/submissions/16/publications/__publicationId__/contributors', + 'https://httbin.org/publicknowledge/api/v1/submissions/16/publications/__publicationId__/contributors', fields: [ { ...fieldGivenName, @@ -483,6 +496,9 @@ export default { alert(msg) { alert(msg); }, + showTab(tab) { + pkp.eventBus.$emit('open-tab', tab); + }, }, created() { pkp.localeKeys['contributor.listPanel.preview.description'] = diff --git a/src/docs/components/WorkflowPage/readme.md b/src/docs/components/WorkflowPage/readme.md index 57c4f7063..7ba73d841 100644 --- a/src/docs/components/WorkflowPage/readme.md +++ b/src/docs/components/WorkflowPage/readme.md @@ -1,5 +1,7 @@ ## Data +This is a root component. Learn about [page hydration](#/pages/pages). + | Key | Description | | --- | --- | | `activityLogLabel` | Label for the activity log | @@ -31,10 +33,4 @@ ## Usage -The `WorkflowPage` extends the [`Container`](/#/pages/container) app. Use this app to show the submission workflow. - -## Template - -The `WorkflowPage` app is a template-less component. You must write the template in Smarty on the server-side and it will be compiled at run time. - -The example here provides a sample template. +The `WorkflowPage` extends the [Page](#/component/Page) component. Use this app to show the submission workflow. diff --git a/src/docs/data/categories.js b/src/docs/data/categories.js index a8005ae66..1db8f42ed 100644 --- a/src/docs/data/categories.js +++ b/src/docs/data/categories.js @@ -1,98 +1,8 @@ -export default [ - { - contextId: 1, - description: { - en: '', - fr_CA: '', - }, - id: 1, - parentId: null, - path: 'applied-science', - sequence: 1, - sortOption: 'datePublished', - title: { - en: 'Applied Science', - fr_CA: '', - }, - }, - { - contextId: 1, - description: { - en: '', - fr_CA: '', - }, - id: 2, - parentId: 1, - path: 'computer-science', - sequence: 2, - sortOption: 'sequence', - title: { - en: 'Computer Science', - fr_CA: '', - }, - }, - { - contextId: 1, - description: { - en: '', - fr_CA: '', - }, - id: 3, - parentId: 1, - path: 'engineering', - sequence: 3, - sortOption: 'datePublished', - title: { - en: 'Engineering', - fr_CA: '', - }, - }, - { - contextId: 1, - description: { - en: '', - fr_CA: '', - }, - id: 4, - parentId: null, - path: 'social-sciences', - sequence: 2, - sortOption: 'title', - title: { - en: 'Social Sciences', - fr_CA: '', - }, - }, - { - contextId: 1, - description: { - en: '', - fr_CA: '', - }, - id: 5, - parentId: 4, - path: 'sociology', - sequence: 1, - sortOption: 'title', - title: { - en: 'Sociology', - fr_CA: '', - }, - }, - { - contextId: 1, - description: { - en: '', - fr_CA: '', - }, - id: 6, - parentId: 4, - path: 'anthropology', - sequence: 2, - sortOption: 'title', - title: { - en: 'Anthropology', - fr_CA: '', - }, - }, -]; +export default { + 1: 'Applied Science', + 2: 'Applied Science > Computer Science', + 3: 'Applied Science > Engineering', + 4: 'Social Sciences', + 5: 'Social Sciences > Sociology', + 6: 'Social Sciences > Anthropology', +}; diff --git a/src/docs/data/emailTemplate.js b/src/docs/data/emailTemplate.js index 3edc369d1..12900aac3 100644 --- a/src/docs/data/emailTemplate.js +++ b/src/docs/data/emailTemplate.js @@ -1,6 +1,6 @@ export default { _href: - 'http://localhost:8000/publicknowledge/api/v1/emailTemplates/EDITOR_DECISION_ACCEPT', + 'https://httbin.org/publicknowledge/api/v1/emailTemplates/EDITOR_DECISION_ACCEPT', body: { en: `<p>Dear {$senderName},</p> <p>I am delighted to inform you that your submission, {$submissionTitle}, has been accepted for publication. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> @@ -19,8 +19,7 @@ export default { }, stageId: 3, subject: { - en: - 'Your submission has been accepted for publication in {$journalName}', + en: 'Your submission has been accepted for publication in {$journalName}', fr_CA: 'Votre soumission a été acceptée pour publication dans {$journalName}', }, diff --git a/src/docs/data/issue.js b/src/docs/data/issue.js index 586c0fada..45c63749d 100644 --- a/src/docs/data/issue.js +++ b/src/docs/data/issue.js @@ -2,7 +2,7 @@ import submission from '@/docs/data/submission'; import doi from '@/docs/data/doi'; export default { - _href: 'http://localhost:8000/publicknowledge/api/v1/issues/1', + _href: 'https://httbin.org/publicknowledge/api/v1/issues/1', articles: [ { ...submission, @@ -15,8 +15,7 @@ export default { authorsString: 'Catherine Kwantes', authorsStringShort: 'Kwantes', fullTitle: { - en: - 'Quisque vel ultrices ut vel sollicitudin vel varius suscipit phasellus', + en: 'Quisque vel ultrices ut vel sollicitudin vel varius suscipit phasellus', }, isPublished: true, }, @@ -58,7 +57,7 @@ export default { isPublished: '1', number: '1', 'pub-id::doi': '10.987/iss123', - publishedUrl: 'http://localhost:8080/publicknowledge/issue/view/1', + publishedUrl: 'https://httbin.org/publicknowledge/issue/view/1', title: { en: 'Issue Number 1', fr_CA: 'Issue Number 1', diff --git a/src/docs/data/publication.js b/src/docs/data/publication.js index f51976dca..b186cc634 100644 --- a/src/docs/data/publication.js +++ b/src/docs/data/publication.js @@ -3,10 +3,9 @@ import doi from '@/docs/data/doi'; export default { _href: - 'http://localhost:8000/publicknowledge/api/v1/submissions/17/publications/17', + 'https://httbin.org/publicknowledge/api/v1/submissions/17/publications/17', abstract: { - en: - '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>', + en: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>', fr_CA: '<p>FR Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>', }, @@ -39,8 +38,7 @@ export default { }, doiObject: {...doi}, fullTitle: { - en: - 'The Lorem ipsum dolor sit amet: Excepteur sint occaecat cupidatat non proident', + en: 'The Lorem ipsum dolor sit amet: Excepteur sint occaecat cupidatat non proident', fr_CA: 'Le Frorem ipsum dolor sit amet: Frexcepteur sint occaecat cupidatat non proident', }, @@ -139,5 +137,5 @@ export default { en: '', }, urlPublished: - 'http://localhost:8000/publicknowledge/article/view/lorem-ipsum/version/17', + 'https://httbin.org/publicknowledge/article/view/lorem-ipsum/version/17', }; diff --git a/src/docs/data/reviewer.js b/src/docs/data/reviewer.js index cbba517ff..4b844d12b 100644 --- a/src/docs/data/reviewer.js +++ b/src/docs/data/reviewer.js @@ -46,7 +46,7 @@ export default { reviewsActive: 2, reviewsCompleted: 1, averageReviewCompletionDays: 21, - dateLastReviewAssignment: '2019-02-03 11:22:42', + dateLastReviewAssignment: '2023-02-03 11:22:42', reviewsDeclined: 0, reviewsCancelled: 0, reviewerRating: 4, diff --git a/src/docs/data/submission.js b/src/docs/data/submission.js index 277867d40..85c9e96f6 100644 --- a/src/docs/data/submission.js +++ b/src/docs/data/submission.js @@ -1,7 +1,7 @@ import publication from './publication'; export default { - _href: 'http://localhost:8000/publicknowledge/api/v1/submissions/1', + _href: 'https://httbin.org/publicknowledge/api/v1/submissions/1', contextId: 1, currentPublicationId: 17, dateLastActivity: '2019-06-25 16:52:47', @@ -11,6 +11,7 @@ export default { publications: [{...publication}], reviewAssignments: [], reviewRounds: [], + sectionId: 1, stageId: 1, stages: [ { @@ -61,9 +62,8 @@ export default { statusLabel: 'Queued', submissionProgress: '', urlAuthorWorkflow: - 'http://localhost:8000/publicknowledge/authorDashboard/submission/1', - urlEditorialWorkflow: - 'http://localhost:8000/publicknowledge/workflow/access/1', - urlPublished: 'http://localhost:8000/publicknowledge/article/view/1', - urlWorkflow: 'http://localhost:8000/publicknowledge/workflow/access/1', + 'https://httbin.org/publicknowledge/authorDashboard/submission/1', + urlEditorialWorkflow: 'https://httbin.org/publicknowledge/workflow/access/1', + urlPublished: 'https://httbin.org/publicknowledge/article/view/1', + urlWorkflow: 'https://httbin.org/publicknowledge/workflow/access/1', }; diff --git a/src/docs/data/submissions.js b/src/docs/data/submissions.js index d25d75145..4080d7013 100644 --- a/src/docs/data/submissions.js +++ b/src/docs/data/submissions.js @@ -21,12 +21,12 @@ export default [ crossref_failedMsg: 'This is a sample failure message', }, fullTitle: { - en: - 'Quisque vel ultrices ut vel sollicitudin vel varius suscipit phasellus', + en: 'Quisque vel ultrices ut vel sollicitudin vel varius suscipit phasellus', }, isPublished: true, }, ], + sectionId: 2, stages: submission.stages.map((stage) => { return { ...stage, @@ -81,8 +81,7 @@ export default [ authorsString: '', authorsStringShort: '', fullTitle: { - en: - 'Submission title when current user is assigned as reviewer and editor', + en: 'Submission title when current user is assigned as reviewer and editor', }, }, ], @@ -128,8 +127,7 @@ export default [ authorsString: 'Convallis Tellus', authorsStringShort: 'Tellus', fullTitle: { - en: - 'Scelerisque felis imperdiet proin fermentum: Pretium quam vulputate dignissim suspendisse in est', + en: 'Scelerisque felis imperdiet proin fermentum: Pretium quam vulputate dignissim suspendisse in est', }, }, ], @@ -156,6 +154,7 @@ export default [ statusId: pkp.const.REVIEW_ASSIGNMENT_STATUS_RECEIVED, }, ], + sectionId: 2, stages: submission.stages.map((stage) => { if (stage.id === pkp.const.WORKFLOW_STAGE_ID_SUBMISSION) { return { @@ -343,8 +342,7 @@ export default [ authorsString: 'Ullamco Excepteur', authorsStringShort: 'Excepteur', fullTitle: { - en: - 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur', + en: 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur', }, }, ], @@ -404,6 +402,7 @@ export default [ statusId: pkp.const.REVIEW_ASSIGNMENT_STATUS_COMPLETE, }, ], + sectionId: 2, stages: submission.stages.map((stage) => { if (stage.id === pkp.const.WORKFLOW_STAGE_ID_SUBMISSION) { return { diff --git a/src/docs/utilities/Dialog/ComponentDialog.vue b/src/docs/mixins/Dialog/ComponentDialog.vue similarity index 100% rename from src/docs/utilities/Dialog/ComponentDialog.vue rename to src/docs/mixins/Dialog/ComponentDialog.vue diff --git a/src/docs/utilities/Dialog/previews/PreviewDialog.vue b/src/docs/mixins/Dialog/previews/PreviewDialog.vue similarity index 100% rename from src/docs/utilities/Dialog/previews/PreviewDialog.vue rename to src/docs/mixins/Dialog/previews/PreviewDialog.vue diff --git a/src/docs/utilities/Dialog/readme.md b/src/docs/mixins/Dialog/readme.md similarity index 69% rename from src/docs/utilities/Dialog/readme.md rename to src/docs/mixins/Dialog/readme.md index 83e226086..cf87f088f 100644 --- a/src/docs/utilities/Dialog/readme.md +++ b/src/docs/mixins/Dialog/readme.md @@ -1,30 +1,6 @@ -## Props - -| Key | Description | -| --- | --- | -| `name` | A unique name for this dialog. | -| `title` | The title to display in the dialog. | -| `message` | The message to display in the dialog. | -| `close` | A callback function that will be fired when the dialog is closed. | -| `actions` | The buttons to add to the dialog. | -| `actions[0].label` | The label for the button. | -| `actions[0].callback` | A callback function that will be fired when the button is pressed. | -| `actions[0].isPrimary` | Whether to style this action as the primary or main action. See [Button](#/component/Button) | -| `actions[0].isWarnable` | Whether to style this action like a cancel, back or delete action. See [Button](#/component/Button) | -| `actions[0].element` | Pass `a` to make this a link instead of a button. See [Button](#/component/Button) | -| `actions[0].href` | The URL of a link when `element` is set to `a`. See [Button](#/component/Button) | - -## Events - -This component does not emit any events. - -## Global Events - -This component does not emit any global events. - ## Usage -Dialogs provide a quick way to show a simple confirmation prompt. Import the `dialog` mixin and use the `openDialog` method to create a dialog. +Dialogs provide a quick way to show a simple confirmation prompt. Import the `dialog` mixin and use the `openDialog()` method to create a dialog. ```js import dialog from '@/mixins/dialog'; @@ -48,9 +24,7 @@ export default { label: 'No', isWarnable: true, callback: () => { - // user has cancelled - // usually you want to close the modal - this.$modal.hide('example'); + // user has cancelled. close the modal } } ], @@ -62,4 +36,26 @@ export default { } ``` -Dialogs should usually have buttons to confirm or cancel an action. Dialogs should never have more than three buttons. \ No newline at end of file +Dialogs use [vue-js-modal](https://github.com/euvl/vue-js-modal). Use the helper method to close a dialog by it's `name`. + +```js +this.$modal.hide('example'); +``` + +## openDialog() + +The `openDialog()` method accepts a configuration object with the following parameters. + +| Key | Description | +| --- | --- | +| `name` | A unique name for this dialog. | +| `title` | A localized title to display in the dialog. | +| `message` | A localized message to display in the dialog. | +| `close` | A callback function that will be fired when the dialog is closed. | +| `actions` | An array of buttons to add to the dialog. | +| `actions[0].label` | The label for the button. | +| `actions[0].callback` | A callback function that will be fired when the button is pressed. | +| `actions[0].isPrimary` | Whether to style this action as the primary or main action. See [Button](#/component/Button) | +| `actions[0].isWarnable` | Whether to style this action like a cancel, back or delete action. See [Button](#/component/Button) | +| `actions[0].element` | Pass `a` to make this a link instead of a button. See [Button](#/component/Button) | +| `actions[0].href` | The URL of a link when `element` is set to `a`. See [Button](#/component/Button) | diff --git a/src/docs/mixins/Fetch/ComponentFetch.vue b/src/docs/mixins/Fetch/ComponentFetch.vue new file mode 100644 index 000000000..675744f71 --- /dev/null +++ b/src/docs/mixins/Fetch/ComponentFetch.vue @@ -0,0 +1,23 @@ +<script> +import Component from '@/docs/Component.vue'; +import Preview from './previews/PreviewFetch.vue'; +import PreviewTemplate from '!raw-loader!./previews/PreviewFetch.vue'; +import readme from '!raw-loader!./readme.md'; + +export default { + extends: Component, + data() { + return { + name: 'Fetch', + readme: readme, + examples: [ + { + component: Preview, + name: 'Fetch', + template: this.extractTemplate(PreviewTemplate), + }, + ], + }; + }, +}; +</script> diff --git a/src/docs/mixins/Fetch/previews/PreviewFetch.vue b/src/docs/mixins/Fetch/previews/PreviewFetch.vue new file mode 100644 index 000000000..492ba3fa8 --- /dev/null +++ b/src/docs/mixins/Fetch/previews/PreviewFetch.vue @@ -0,0 +1,92 @@ +<template> + <div class="previewFetch"> + <form class="previewFetch__form"> + <fieldset class="previewFetch__field"> + <legend>Show items in:</legend> + <label v-for="(name, id) in sections" :key="id"> + <input type="checkbox" v-model="sectionsSelected" :value="id" /> + {{ name }} + </label> + </fieldset> + <div class="previewFetch__field"> + <label> + Search: + <input type="search" v-model="searchPhrase" /> + </label> + </div> + </form> + <ul> + <li v-for="submission in submissions" :key="submission.id"> + {{ sections[submission.sectionId] }} + — + {{ submission.publications[0].authorsStringShort }} + — + {{ submission.publications[0].fullTitle.en }} + </li> + </ul> + </div> +</template> + +<script> +import fetch from '@/mixins/fetch'; +import submissions from '../../../data/submissions'; + +export default { + mixins: [fetch], + data() { + return { + searchPhrase: '', + sections: { + 1: 'Articles', + 2: 'Reviews', + }, + sectionsSelected: [], + submissions: [...submissions], + }; + }, + methods: { + /** + * Overwrite the `get()` method of the + * fetch mixin to filter the submissions + * locally + */ + get() { + const searchPhrase = ( + this.activeFilters.searchPhrase ?? '' + ).toLowerCase(); + const sectionIds = this.activeFilters.sectionIds ?? []; + this.submissions = [...submissions] + .filter((s) => { + return ( + !searchPhrase || + s.publications[0].fullTitle.en.toLowerCase().match(searchPhrase) || + s.publications[0].authorsStringShort + .toLowerCase() + .match(searchPhrase) + ); + }) + .filter((s) => { + return !sectionIds.length || sectionIds.includes(s.sectionId); + }); + }, + }, + watch: { + searchPhrase(newVal, oldVal) { + this.activeFilters = { + ...this.activeFilters, + searchPhrase: newVal, + }; + }, + sectionsSelected(newVal, oldVal) { + this.activeFilters = { + ...this.activeFilters, + sectionIds: newVal.map((id) => parseInt(id, 10)), + }; + }, + }, +}; +</script> + +<style lang="less"> +@import '../../../../styles/_import'; +</style> diff --git a/src/docs/mixins/Fetch/readme.md b/src/docs/mixins/Fetch/readme.md new file mode 100644 index 000000000..cc4f003ea --- /dev/null +++ b/src/docs/mixins/Fetch/readme.md @@ -0,0 +1,176 @@ +# Usage + +The `fetch` mixin provides helpers to get data from most REST API endpoints in the application. With a few props it can make requests to the API with support for filters, searching, and pagination. Any component that uses this mixin must define a `setItems()` method. + + +```js +import fetch from '@/mixins/fetch'; + +export default { + mixins: [fetch] + methods: { + /** + * Save the `items` and `itemsMax` properties + * returned by the request to the REST API to + * the component's local data. + * + * @param {Array} items + * @param {Number} itemsMax + */ + setItems(items, itemsMax) { + this.items = items; + this.itemsMax = itemsMax; + } + } +} +``` + +Call the `get()` method to fetch the items. + +```html +<template> + <button @click="getItems">Load</button> +</template> + +<script> +export default { + methods: { + getItems() { + this.get(); + } + } +} +</script> +``` + +The `get()` method will be called automatically when the `activeFilters` and `searchPhrase` are changed. + +```html +<template> + <button @click="getItems"> + Filter by items in section 1 with the + phrase "lorem" + </button> +</template> + +<script> +export default { + methods: { + getItems() { + this.activeFilters = { + sectionIds: [1] + }; + this.searchPhrase = 'lorem'; + } + } +} +</script> +``` + +## Props + +All components implementing this mixin have the following props. + +| Key | Description | +| --- | --- | +| `apiUrl` | **Required** The URL to the API endpoint to use when retrieving items. | +| `count` | The number of items to return with each request. Default: `30` | +| `getParams` | Any query params that should be added to every request. For example, if you are showing a list of declined submissions, use `{statusIds: [pkp.const.STATUS_DECLINED]}`. | +| `lazyLoad` | Pass `true` to load the items as soon as the component is mounted. Default: `false` | + +## Data + +All components implementing this mixin have the following data. + +| Key | Description | +| --- | --- | +| `activeFilters` | An object describing any filters that are currently active. These are the query params that should be added to the `GET` request. Whenever `activeFilters` is modified, the list will be reloaded. | +| `isLoading` | Are new items currently being loaded? | +| `itemsMax` | The number of possible items in the list for calculating pagination. | +| `offset` | Used with `count` to support pagination. | +| `searchPhrase` | A search phrase to add to the query params when requesting items. Whenever the search phrase changes, the list will be reloaded. | + +## Computed Properties + +All components implementing this mixin have the following computed properties. + +| Key | Description | +| --- | --- | +| `currentPage` | The current page being shown, based on `offset` and `count`. | +| `lastPage` | The last page available, based on `count` and `itemsMax`. | + +## Methods + +All components implementing this mixin can use the following methods. + +| Name | Description | +| --- | --- | +| `get()` | Call this method whenever the list should be reloaded. It is called automatically when `activeFilters` or `searchPhrase` are changed. | +| `setPage(page)` | Change the current page being shown in the list. | +| `setSearchPhrase(searchPhrase)` | Change the value of the search phrase. | + +## Search + +Use a [`Search`](#/component/Search) component when implementing search with this mixin. + +```html +<template> + <div> + <search + :searchPhrase="searchPhrase" + @search-phrase-changed="setSearchPhrase" + /> + </div> +</template> +``` + +See an example of a [List Panel with Search](#/component/ListPanel/with-search) + +## Filters + +Filters are used to change which items appear in a list. They interact with the REST API query params and the `activeFilters` prop should always reflect the query params you wish to use. In the example below, a component will toggle the view from all submissions to overdue submissions by adding and removing an active filter. + +```js +export default { + methods: { + addOverdueFilter() { + this.activeFilters = { + ...this.activeFilters, + isOverdue: true + }; + } + removeOverdueFilter() { + let activeFilters = {...this.activeFilters); + delete activeFilters.isOverdue; + this.activeFilters = activeFilters; + } + } +} +``` + +See an example of a [List Panel with Filters](#/component/ListPanel/with-filter). + +## Pagination + +Use the [`Pagination`](#/component/Pagination) component to break long lists of results into pages. + +```html +<template> + <div> + ... + <pagination + v-if="lastPage > 1" + :currentPage="currentPage" + :isLoading="isLoading" + :lastPage="lastPage" + @set-page="setPage" + /> + </div> +</template> +``` + +See an example of a [List Panel with Pagination](#/component/ListPanel/with-pagination). + +## Lazy-load + +You may wish to wait to load the items until a component has been mounted. In this case, set the `lazyLoad` prop to `true` and the items will be lazy-loaded. In most cases you should pre-populate your list from the server. Only use the `lazyLoad` prop when you want to reduce the time it takes for the initial page load, and the list items are not the first priority on the page. diff --git a/src/docs/pages/accessibility.md b/src/docs/pages/accessibility.md index f267fa71a..78eef6f27 100644 --- a/src/docs/pages/accessibility.md +++ b/src/docs/pages/accessibility.md @@ -1,34 +1,29 @@ # Accessibility -Some components require specific markup or special handling based on state to ensure that they are accessible. Component-specific guidelines are outlined within each component's Notes section. The following are broad accessibility guidelines which should be followed. +All components in the library are written to be accessible to everyone, regardless of whether that person is using a mouse and keyboard, a touch device, or assistive technology like a screen reader. Sometimes, when using a component, it is important to know how to use it correctly to take advantage of its accessibility support. The demo page for each component will include accessibility guidance where appropriate. However, some things are important to know when working with any component. -## Focus state and keyboard-based navigation +## Navigate by keyboard -It should be possible to use a component without a mouse. To test this, use the <kbd>TAB</kbd> key to interact with the component and check the following: +It should be possible to use a component without a mouse or touch device. To test this, use the <kbd>TAB</kbd> key to interact with the component and check the following: -- Can I use the component? For example, if the component allows reordering items, can I do this with a keyboard? -- Can I see where the focus is and is it easy to determine where it moves each time I hit the <kbd>TAB</kbd> key? The focus should never "disappear". +- Can I use all features of the component with the keyboard? For example, if the component allows the user to reorder items, can I do this with a keyboard? +- Can I see where the focus is at all times? Is it easy to see where it moves each time I hit the <kbd>TAB</kbd> key? Does the focus ever "disappear" from the page, so that I don't know where it is? -### Moving focus +## Don't switch focus (usually) -As a general rule, you should *never* move the focus programmatically. This can be disorienting and frustrating for users who rely on keyboard navigation. +Avoid moving the focus from one element to another in the code. This can be disorienting and frustrating for users who rely on keyboard navigation. We make an exception when the user performs an action that can be interpreted as a request to move focus. For example, when a button is pressed to open a modal, the focus should be moved into that modal and kept there until the modal is closed. When closed, the focus should be moved to the element which opened the modal. The [Modal](#/component/Modal) component will automatically "take" the focus when it is opened. But guidance in the component demo should be followed to handle focus when it is closed. -We make an exception for this when the user performs an action which indicates a particular focus is desired. +Another common example is when an action deletes an item on the page. If the focus is on a delete button when that button is removed, the focus will be dropped to the window. The user will be forced to navigate again all the way to the part of the page they were at before. In such cases, it is often best to reset the focus to a reasonable nearby element. For example, move the focus to a previous item in a list or the title of the section where the item was removed. -For example, when a button is pressed to open a modal, the focus should be moved into that modal and kept there until the modal is closed. When closed, the focus should be returned to the element which opened the modal. +## Don't use tabindex -### Avoid tabindex +Avoid using the `tabindex` attribute to change the tab order. This is disorienting for users accustomed to navigating by keyboard. An exception can be made when you need to use `tabindex="-1"` to prevent an item from receiving focus. You may wish to do this to remove drag-and-drop controls from a keyboard user's tab order, or to prevent the user from having to tab through elements which are currently hidden. Otherwise, never try to set the tab order using `tabindex`. -Avoid using the `tabindex` attribute. An exception can be made when you need to use `tabindex="-1"` to prevent an item from receiving focus. You may wish to do this to remove drag-and-drop controls from a keyboard user's tab order, or to prevent the user from having to tab through elements which are currently hidden. +## Use labels for screen readers -## Labelling for assistive technology +When a component uses icons or a visual layout to indicate the meaning of something, you must provide a text label for users without sight. Use the `-screenReader` class to add a label for screen readers that can't be seen on the screen. Never use `display: hidden`. Screen readers will ignore the text. -When a component uses icons or a visual layout to indicate the meaning of something, you must provide a text label for users without sight. You can use the `-screenReader` class to hide the label from sighted users but still expose it to screen readers. - -You should _not_ use `display: hidden`, because screen readers will ignore the text. - -### Hidden search label -A search field often uses an icon to indicate it's purpose. The `placeholder` attribute can not be read by all screen readers, so a label is provided and then hidden from sighted users. +For example, a search field often uses an icon of a magnifying glass to provide a hint that it is a search field, along with the `placeholder` attribute. However, a `placeholder` attribute can not be read by all screen readers. Provide a text `<label>` that is visually hidden. <div class="inlinePreview inlinePreview--accessibleSearch"> <span class="fa fa-search" aria-hidden="true"></span> @@ -46,8 +41,7 @@ A search field often uses an icon to indicate it's purpose. The `placeholder` at </label> ``` -### Icon-only buttons -In a few cases, an icon may not need a text label for sighted users to understand it's purpose. A hidden text label that indicates the action and which item the action relates to must be provided for those using screen readers. +An icon may not need a text label for sighted users to understand it's purpose. For example, when a ✘ icon appears to remove an item. In such cases, a hidden text label must be provided that indicates the action and which item the action relates to. <div class="inlinePreview inlinePreview--accessibleButton"> <div> @@ -68,3 +62,5 @@ In a few cases, an icon may not need a text label for sighted users to understan </button> </li> ``` + +Learn more about how to [contribute new components](#/pages/contributing). \ No newline at end of file diff --git a/src/docs/pages/autosave.md b/src/docs/pages/autosave.md new file mode 100644 index 000000000..0548a26f1 --- /dev/null +++ b/src/docs/pages/autosave.md @@ -0,0 +1,109 @@ +# autosave + +This mixin helps to add an autosave feature to a component. It implements a pattern to queue autosaves, process them, and handle loss of connection events. When there is no connection to the server, autosaves will be stored in local storage and replayed when the connection is restored. Autosaves can be restored even after the user has closed the browser and returned later. + +## Required Properties + +Every component that uses the autosave mixin must implement the following properties. + +| Key | Description | +| --- | --- | +| `autosavesKey` | A unique key to store autosaves in local storage. The key must be unique to the action being saved. For example, the submission wizard uses an autosave key that includes the submission id to prevent collisions between autosaves for different submissions. | +| `i18nDiscardChanges` | A localized string for the button to discard changes when unsaved changes are found. | +| `i18nUnsavedChanges` | A localized string for the title of the dialog that appears when unsaved changes are found in local storage. | +| `i18nUnsavedChangesMessage` | A localized string for the message of the dialog that appears when unsaved changes are found in local storage. | + +## Required Methods + +Every component that uses this mixin must implement the following methods. + +| Name | Description | +| --- | --- | +| `addAutosaves()` | This method is called whenever the autosave mixin thinks that stale data may need to be autosaved. It should check for any stale data and use `addAutosave()` to add any data that needs to be saved. See usage guidance below. | +| `restoreStoredAutosave(payload)` | This method is called whenever autosaves stored in local storage are replayed. Use this method to update the UI with the data from the stored autosaves. For example, if a stored autosave included a change to the title that was never saved, you may need to apply this change to a title field in a form on the page. | + +## Optional Methods + +Every component that uses this mixin may add the following methods used by this mixin. + +| Name | Description | +| --- | --- | +| `autosaveSucceeded(payload, response)` | This method is called whenever an autosave succeeds. Use this method to sync response data from the server with state data. | +| `autosaveErrored(payload, xhr, status)` | This method is called whenever an autosave fails. Use this method to display a warning or handle authentication errors, for example if a user has been logged out. Not all errors need an action. For example, this method will be called when connection is lost. But the mixin should handle such scenarios by itself. | + + + +## Usage + +Import the mixin and add an autosave whenever data changes. + +```js +import autosave from '@/mixins/autosave'; + +export default { + mixins: [autosave], + props: ['name', 'email'], + data() { + return { + autosavesKey: 'example', + i18nDiscardChanges: 'Discard Changes', + i18nUnsavedChanges: 'Unsaved Changes', + i18nUnsavedChangesMessage: 'We found unsaved changes from {$when}. Would you like to restore those changes now?', + }; + }, + methods: { + addAutosaves() { + if (/* data is stale */) { + this.formChanged(); + } + }, + restoreStoredAutosave(payload) { + this.name = payload.data.name ?? this.name; + this.email = payload.data.email ?? this.email; + }, + formChanged() { + this.addAutosave( + 'nameChanged', + 'https://example.org/api/v1/users/1, + { + name: this.name, + email: this.email, + } + ); + } + } +} +``` + +Usually, you don't want to autosave as soon as data changes. Someone typing a name with 20 characters will send 20 HTTP requests. Use `debounce` to only send an autosave after the user stops changing the data. + +```js +import autosave from '@/mixins/autosave'; +import debounce from 'debounce'; + +export default { + mixins: [autosave], + + ... + + methods: { + + formChanged: debounce( + function() { + this.addAutosave( + 'nameChanged', + 'https://example.org/api/v1/users/1, + { + name: this.name, + email: this.email, + } + ); + }, + 5000 // Wait until input has stopped for 5 seconds + ) + } +} +``` + + + diff --git a/src/docs/pages/container.md b/src/docs/pages/container.md deleted file mode 100644 index a3cd884a6..000000000 --- a/src/docs/pages/container.md +++ /dev/null @@ -1,65 +0,0 @@ -# Container - -A `Container` is a root component to initialize Vue.js in a template. In most cases this is not needed. Instead, a `Page` component is automatically initialized on every page in the editorial backend. Learn more about [Pages](#/component/Page). - -However, it may be necessary to initialize Vue.js in a template that doesn't yet support the `Page` component. This is the case when content is loaded into a modal in the older JS framework. - -## Integration with legacy JavaScript framework - -The following describes how to use the `Container` directly when integrating with an old modal. - -In the example below, the `Container` component treats the content of the `#example-container-{$uuid}` DOM element as the template. It creates a `<pkp-form>` component and passes the value of `this.components.masthead` as props. - -```html -{assign var="uuid" value=""|uniqid|escape} -<div id="example-container-{$uuid}"> - <pkp-form - v-bind="components.masthead" - @set="set" - /> -</div> -<script type="text/javascript"> - pkp.registry.init('example-container-{$uuid}', 'Container', {$exampleData|json_encode}); -</script> -``` - -This is the same as mounting a Vue app to the DOM as recommended in Vue's documentation. - -```js -var app = new Vue({ - ...Container, - el: '#example-container-<uuid>', - data: exampleContainerDataInJsonFormat -}) -``` - -We mount Vue apps using `pkp.registry.init` to make sure that Vue components mounted inside of our legacy JavaScript toolkit do not cause memory leaks. - -## Child Components - -Attach as many components to a `Container` as you would like, by passing a `components` array with the initial data. - -```php -$templateMgr->assign('exampleData', [ - 'components' => [ - 'masthead' => [...], - 'submissions' => [...], - ] -]); -``` -```html -{assign var="uuid" value=""|uniqid|escape} -<div id="example-container-{$uuid}"> - <pkp-form - v-bind="components.masthead" - @set="set" - /> - <submissions-list-panel - v-bind="components.submissions" - @set="set" - /> -</div> -<script type="text/javascript"> - pkp.registry.init('example-container-{$uuid}', 'Container', {$exampleData|json_encode}); -</script> -``` \ No newline at end of file diff --git a/src/docs/pages/contributing.md b/src/docs/pages/contributing.md index b8dadb2e5..209db0e2d 100644 --- a/src/docs/pages/contributing.md +++ b/src/docs/pages/contributing.md @@ -1,39 +1,40 @@ # Contributing -This library provides documentation and a development sandbox for building and maintaining components. If you contribute you may be asked to document your changes. This document describes how to add and edit documentation for a component. +When creating a new component, you may be asked to document it in this library. You will need to create a component demo, document the component, and add it to the site. The following provides an outline of how the library files are structured and how to add a component demo to the library. -## File Structure +## Source Files -The following directories contain code that is run by our applications. +The following directories contain the source components, mixins and styles that make up the UI Library components. Files in these directories may be imported and used as components in the applications. -| Directory | Description | -| --- | --- | -| `/src/components` | All of the Vue.js component source files. | -| `/src/mixins` | [Mixins](https://vuejs.org/v2/guide/mixins.html) used in components. | -| `/src/styles` | LESS styles that are imported into global stylesheets and components. | +| Directory | Description | +| ----------------- | ---------------------------------------------------------------------------------- | +| `/src/components` | [Single-File Component](https://vuejs.org/guide/scaling-up/sfc.html) source files. | +| `/src/mixins` | [Mixins](https://vuejs.org/v2/guide/mixins.html) used in components. | +| `/src/styles` | Global CSS/LESS styles, like variables and resets. | -The rest of the files in this library are used to run the component demos. +## Library Files -| Directory | Description | -| --- | --- | -| `/public` | Static site files. | -| `/src/docs` | Document and preview components. | -| `/src/docs/components` | Document individual components in the UI Library. | -| `/src/docs/pages` | Markdown files for pages that are not related to a component, such as the page you are reading now. | -| `/src/App.vue` | The main component that runs the UI Library. | -| `/src/main.js` | Loads dependencies and initializes the UI Library. | -| `/src/router.js` | The router for the UI Library. | +The following files and directories contain the UI Library itself: the documentation, demo components, and application that make up this website. -## Create a Component Demo +| Directory | Description | +| ---------------------- | ------------------------------------------------------------ | +| `/public` | Static files and sample data. | +| `/src/docs` | All documentation and demonstration components. | +| `/src/docs/components` | Component demo for the components in `/src/components`. | +| `/src/docs/pages` | Documentation not related to a component, such as this page. | +| `/src/App.vue` | The root component of the UI Library. | +| `/src/main.js` | Loads dependencies and initializes the UI Library. | +| `/src/router.js` | The router for the UI Library. | -Each component demo should include an example of the component in action, documentation describing the props accepted and events emitted by the component, and guidance on when and how the component should be used. +## Add a Component Demo -Create a single-file Vue component that loads an example of the component you wish to document: +A component demo is just a wrapper component that loads the source component and passes props to it. The following example shows a demo of the [Notification](#/component/Notification) component. ```html -<!-- /src/docs/components/Notification/previews/PreviewNotification.vue --> +<!-- /src/docs/components/Notification/PreviewNotification.vue --> + <template> - <div class="previewNotification"> + <div> <notification type="warning"> This submission does not have an editor assigned. </notification> @@ -41,7 +42,7 @@ Create a single-file Vue component that loads an example of the component you wi </template> <script> -import Notification from '@/components/Notification/Notification.vue'; +import Notification from '../../components/Notification/Notification.vue'; export default { components: { @@ -51,13 +52,13 @@ export default { </script> ``` -Create a `readme.md` file that documents how the component works. +Place the demo into the `src/docs/components/Notification` directory and create a `readme.md` file in the same directory. ```md ## Props -| Key | Description | -| --- | --- | +| Key | Description | +| ------ | -------------------------------------------------------------------------------------------- | | `type` | The type of notification. Pass `warning` for notifications about errors or serious problems. | ## Events @@ -69,14 +70,15 @@ This component does not emit any events. Use the `Notification` component to draw the user's attention to new information. Do not overuse notifications. If they become too common, they will no longer draw the user's attention. ``` -Create the documentation component which will load the example and readme: +Create a new component that extends `Component`. Load the readme and demo component into this component. ```html <!-- /src/docs/components/Notification/ComponentNotification.vue --> + <script> import Component from '@/docs/Component.vue'; -import PreviewNotification from './previews/PreviewNotification.vue'; -import PreviewNotificationTemplate from '!raw-loader!./previews/PreviewNotification.vue'; +import PreviewNotification from './PreviewNotification.vue'; +import PreviewNotificationTemplate from '!raw-loader!./PreviewNotification.vue'; import readme from '!raw-loader!./readme.md'; export default { @@ -98,8 +100,6 @@ export default { </script> ``` -Import more `Preview*` components and add them to the `examples` array if you want more than one example. - Add a route to the component in `/src/router.js`: ```js @@ -122,17 +122,3 @@ Add a link to the navigation menu in `/src/App.vue`: <li><router-link to="/component/Notification">Notification</router-link></li> ... ``` - -## ESLint and Prettier - -This library runs ESLint and Prettier to maintain code formatting. When you are running `npm run serve`, any file changes will be linted and errors will be reported in the console. - -When you commit, ESLint and Prettier will run. You will not be able to commit if there are ESLint errors. - -## Issues - -Issues for for this library should be opened on the [pkp/pkp-lib](https://github.com/pkp/pkp-lib/issues/) repository. All issues related to OJS and OMP are managed there, including issues with this UI Library. - -## Pull Requests - -Pull Requests for this library should be opened on the [pkp/ui-library](https://github.com/pkp/ui-library/pulls) repository. diff --git a/src/docs/pages/csrf.md b/src/docs/pages/csrf.md index 6ebdb68fb..908195d2a 100644 --- a/src/docs/pages/csrf.md +++ b/src/docs/pages/csrf.md @@ -1,6 +1,6 @@ # CSRF Token -Any component which sends a `PUT`, `POST` or `DELETE` request to an application's REST API _must_ include a [CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery) in the header. +Any component which sends a `PUT`, `POST` or `DELETE` request to the application's REST API _must_ include a [CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery) in the header. Whenever a user is logged in, you can access their CSRF token at `pkp.currentUser.csrfToken`. The code below demonstrates how to add this to the header of a jQuery ajax request. @@ -11,4 +11,18 @@ $.ajax({ }, ... }); -``` \ No newline at end of file +``` + +The code below demonstrates how to add this head to [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch). + +```js +fetch( + 'https://...', + { + method: 'POST', + headers: { + 'X-Csrf-Token': pkp.currentUser.csrfToken + } + } +); +``` diff --git a/src/docs/pages/event-bus.md b/src/docs/pages/event-bus.md index adaca165f..da66fc94b 100644 --- a/src/docs/pages/event-bus.md +++ b/src/docs/pages/event-bus.md @@ -1,16 +1,14 @@ # Event Bus -Use the event bus to emit and react to global events in the application. This can be useful when you want disparate parts of the application to react to a common change. +Use the event bus to emit and react to global events in the application. This can be useful when you want two components that don't share state to react to the same event. For example, when a submission is deleted you may need to remove it from more than one list of submissions and remove any tasks related to it. -For example, when a submission is deleted you may need to remove it from more than one list of submissions and remove any tasks related to it. - -Use the following to emit a global event. +Emit a global event from any component. ```json pkp.eventBus.$emit('updated:email-template', {...}); ``` -Use the following to react to a global event. +React to the global event from any component. ```js // Update an email template in a list when it changes @@ -26,7 +24,7 @@ pkp.eventBus.$on('updated:email-template', (emailTemplate) => { Global event names should use kebab-case (`example-name`). When appropriate, use the following naming pattern. ```js -<event>:<object> +<action>:<object> ``` Examples include the following. @@ -60,8 +58,6 @@ pkp.eventBus.$on('open-tab', tabId => { ## When to use the global event bus -Use the global event bus whenever data changes, such as when `PUT` or `POST` requests are made, or when a `GET` request updates data that may be shared between components. - -The global event bus is exposed to plugins and third-party code in the global `pkp` variable at `pkp.eventBus`. For this reason, it is an important tool for plugins which want to react to events in the browser. +Use the global event bus whenever data changes, such as when `PUT` or `POST` requests are made, or when a `GET` request updates data that may be shared between components. The global event bus is exposed to plugins and third-party code in the global `pkp` variable at `pkp.eventBus`. For this reason, it is an important tool for plugins which want to react to events in the browser. Even if a global event will not used in the core application, it may be a good idea to emit one. \ No newline at end of file diff --git a/src/docs/pages/fetch.md b/src/docs/pages/fetch.md deleted file mode 100644 index fb0c977c3..000000000 --- a/src/docs/pages/fetch.md +++ /dev/null @@ -1,147 +0,0 @@ -# Fetch - -The `fetch` mixin can be used to interact with a REST API endpoint. It provides a component with props, data and methods to implement lazy-load, filters, search and pagination for a collection of items. - -```js -import fetch from '@/mixins/fetch'; - -export default { - mixins: [fetch] -} -``` - -Load items with the `get()` method. - -```js -export default { - ... - methods: { - refreshList() { - this.get(); - } - } -} -``` - -All components that implement the `fetch` mixin must add a `setItems()` method which receives the items from the API and stores them in local data. - -```js -export default { - ... - methods: { - /** - * Update the list of items - * - * @param {Array} items - * @param {Number} itemsMax - */ - setItems(items, itemsMax) { - this.items = items; - this.itemsMax = itemsMax; - } - } -} -``` - -## Props - -| Key | Description | -| --- | --- | -| `apiUrl` | The URL to the API endpoint to use when retrieving items. | -| `count` | The number of items to return with each request. Default: `30` | -| `getParams` | Any query params that should be added to every request. For example, if you are showing a list of declined submissions, use `{statusIds: [pkp.const.STATUS_DECLINED]}` | -| `lazyLoad` | Pass `true` to fire off a request for items after the component is mounted. Default: `false` | - -## Data - -| Key | Description | -| --- | --- | -| `activeFilters` | An object describing any filters that are currently active. These are `getParams` that may be added or removed. | -| `isLoading` | Are new items currently being loaded? | -| `offset` | Used with `count` to support pagination. | -| `searchPhrase` | A search phrase to add to the query params when requesting items. | - -## Computed Properties - -| Key | Description | -| --- | --- | -| `currentPage` | The current page being shown, based on `offset` and `count`. | -| `lastPage` | The last page available, based on `count` and `itemsMax`. | - -## Search - -Endpoints in PKP's REST API which return a list of items, such as `/submissions`, typically support a `searchPhrase` query param to return items that match the search phrase. - -Add a [`Search`](#/component/Search) component to take advantage of this. - -```html -<template> - <div> - ... - <search - :searchPhrase="searchPhrase" - @search-phrase-changed="setSearchPhrase" - /> - </div> -</template> -``` - -When the `searchPhrase` is changed, the items will automatically be retrieved from the API. The `searchPhrase` will be converted to a query param and the API request will look like `http://.../api-url-endpoint?searchPhrase=<search-phrase-value>`. - -See an example of a [List Panel with Search](#/component/ListPanel/with-search) - -## Filters - -Filters are used to change which items appear in a list. They interact with the REST API query params and the `activeFilters` prop should always reflect the query params you wish to use. - -In the example below, a component will toggle the view from all submissions to overdue submissions by adding and removing an active filter. - -```js -export default { - methods: { - addOverdueFilter() { - let activeFilters = {...this.activeFilters}; - activeFilters.isOverdue = true; - this.activeFilters = activeFilters; - } - removeOverdueFilter() { - let activeFilters = {...this.activeFilters}; - delete activeFilters.isOverdue; - this.activeFilters = activeFilters; - } - } -} -``` - -When the `activeFilters` are changed, the items will automatically be retrieved from the API. The `isOverdue` filter will be converted to a query param and the API request will look like `http://.../api-url-endpoint?isOverdue=1`. - -See an example of a [List Panel with Filters](#/component/ListPanel/with-filter). - -## Pagination - -This mixin provides computed properties and methods to help manage pagination. Add the [`Pagination`](#/component/Pagination) component to take advantage of them. - -```html -<template> - <div> - ... - <pagination - v-if="lastPage > 1" - :currentPage="currentPage" - :isLoading="isLoading" - :lastPage="lastPage" - @set-page="setPage" - /> - </div> -</template> -``` - -When the page is changed, new items will automatically be retrieved from the API. The page request will be converted `count` and `offset` query params and an API request will look like `http://.../api-url-endpoint?count=30&offset=30`. - -See an example of a [List Panel with Pagination](#/component/ListPanel/with-pagination). - -## Lazy-load - -You may wish to wait to load the items until a component has been mounted. In this case, set the `lazyLoad` prop to `true` and the items will be lazy-loaded. - -In most cases you should pre-populate your list from the server. Only use the `lazyLoad` prop when you want to reduce the time it takes for the initial page load, and the list items are not the first priority on the page. \ No newline at end of file diff --git a/src/docs/pages/index.md b/src/docs/pages/index.md index aaaaa811f..2495253e8 100644 --- a/src/docs/pages/index.md +++ b/src/docs/pages/index.md @@ -1,12 +1,9 @@ -# PKP UI Library +# UI Library -This library contains UI components implemented or planned for the Public Knowledge Project's applications [Open Journal Systems](https://pkp.sfu.ca/ojs/), [Open Preprint Systems](https://pkp.sfu.ca/ops/) and [Open Monograph Press](https://pkp.sfu.ca/omp/). +This library contains UI components used by the [Public Knowledge Project](https://pkp.sfu.ca)'s applications [Open Journal Systems](https://pkp.sfu.ca/software/ojs/), [Open Preprint Systems](https://pkp.sfu.ca/software/ops/) and [Open Monograph Press](https://pkp.sfu.ca/software/omp/). It is intended for coders who want to contribute to the software and developers who want to build plugins that integrate with the UI of the editorial backend. -It provides a demonstration of each component, technical documentation, and usage guidance. It is intended for technical developers who want to work with the UI of the editorial backend for one of our applications. +This library is a static site used to document the components and provide a convenient development environment to build and test new components. To learn how to integrate the UI Library with an application's frontend, read our [developer documentation](https://docs.pkp.sfu.ca/dev/documentation/en/frontend). -You do not need to consult this guide unless you want to make substantial changes to the editorial backend. These may include: +The library uses Vue 3, but most components are written using the Vue 2 syntax. In order to make it is easy for the team to maintain the UI Library, it is important to keep the code consistent between components. Use the Vue 2 syntax for new components until a decision is made to migrate all components to the Vue 3 syntax. -- Create a new page with your own components -- Replace an existing page with a customized display -- Maintain or improve the existing components -- Learn how to write your own components +Next, learn more about the [root component](#/pages/pages). diff --git a/src/docs/pages/localStorage.md b/src/docs/pages/localStorage.md new file mode 100644 index 000000000..81352be0e --- /dev/null +++ b/src/docs/pages/localStorage.md @@ -0,0 +1,33 @@ +# localStorage + +This mixin provides helper functions to read and write to the browser's [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Check whether the local storage is enabled in the browser before you read or write to it. + +```js +import localStorage from '@/mixins/localStorage'; + +export default { + mixins: [localStorage], + data() { + return { + preferences: null, + }; + }, + methods: { + set(preferences) { + if (this.isLocalStorageEnabled) { + this.setLocalStorage('user-preferences', preferences); + } + }, + reset() { + if (this.isLocalStorageEnabled) { + this.removeLocaleStorage('user-preferences'); + } + } + }, + mounted() { + if (this.isLocalStorageEnabled) { + this.preferences = this.getLocalStorage('user-preferences'); + } + } +} +``` diff --git a/src/docs/pages/localization.md b/src/docs/pages/localization.md index 233010028..ae79e6300 100644 --- a/src/docs/pages/localization.md +++ b/src/docs/pages/localization.md @@ -1,65 +1,125 @@ -# Working with multiple languages +# Localization -PKP's applications support multiple languages. This means that the application itself can be localized and that data can be entered in more than one language at a time. +Each application can be run in more than one language. This means that UI components must be localized. It also means that data used by the UI components may exist in more than one language. -## Localize text +## Locale Keys -Messages in the application which have been localized to the current user's language are available globally at `pkp.localeKeys`. Use the following to display a localized phrase in a component's template. +Some common localized strings are available in the `pkp.localeKeys` global. Use the `__()` method in any component template. -```js -{{ __('common.cancel') }} -``` - -This can be accessed from component methods by using the following. +<div class="inlinePreview"> + <button> + Cancel + </button> +</div> -```js -const cancel = this.__('common.cancel'); +```html +<template> + <button> + {{ __('common.cancel') }} + </button> +</template> ``` Pass parameters to the localized keys by using the following. -```js -{{ __('list.viewMore', {name: 'Daniel Barnes'}) }} +<div class="inlinePreview"> + <button> + Edit Daniel Barnes + </button> +</div> + +```html +<template> + <button> + {{ __('common.editItem', {item: 'Daniel Barnes'}) }} + </button> +</template> ``` -The application should provide all of the localized phrases necessary from the server. On the server side, a locale key can be added to a page with the following PHP code. +This method can be used in scripts too. + +<div class="inlinePreview"> + <button> + Edit + </button> + <button> + Edit Daniel Barnes + </button> +</div> + +```html +<template> + <button> + {{ cancelLabel }} + </button> + <button> + {{ editItemLabel }} + </button> +</template> + +<script> +export default { + data() { + return { + name: 'Daniel Barnes' + }; + }, + computed: { + cancelLabel() { + return this.__('common.cancel') + }, + editItemLabel() { + return this.__('common.editItem', {item: this.name}) + } + } +} +</script> +``` + +This works for a few common strings that have been defined in `pkp.localeKeys`. This comes from the server and is configured in `PKPTemplateManager`. For other localized strings, a component should accept them as a `prop`. + +```html +<script> +export default { + props: { + i18nNewSubmission: String, + } +} +</script> -```php -$templateMgr = TemplateManager::getManager($request); -$templateMgr->setLocaleKeys([ - 'common.cancel', -]); +<template> + <button> + {{ i18nNewSubmission }} + </button> +</template> ``` -## Multilingual content +## Multilingual Data -Multilingual fields are provided as a JSON object with keys specifying the locale codes. The following response shows the name property of a journal in English and Canadian French. +Multilingual data is usually provided as a JSON object. Each key holds the locale code. The following response shows the name property of a journal in English and Canadian French. ```js { "name": { "en": "Journal of Public Knowledge", "fr_CA": "Journal de la connaissance du public" - }, - ... + } } ``` -The following helper method is available for all components to help you retrieve a value in the correct language. +All components can use the following helper method to get a value in the current language of the UI or, as a fallback, the primary language of the journal, press or preprint server. ```js var name = this.localize(this.name); ``` -This will return the value in the user's current language or the primary language of the site. If it doesn't exist in either of these locales, it will return the first locale it finds. - -You can also request a specific locale. +If the data doesn't exist in either of these locales, it will return the first locale it finds. Pass a second argument to ask for the value in a specific locale. ```js var name = this.localize(this.name, 'fr_CA'); ``` -When working with submissions, a component should use the `localizeSubmission` mixin to localize submission data. This provides a method that will fallback to the submission's primary language rather than the primary language of the site. +When working with submissions, a component should use the `localizeSubmission` mixin to localize submission data. This method will use the submission's primary language as the default value. ```js import localizeSubmission from '@/mixins/localizeSubmission'; @@ -74,3 +134,5 @@ export default { } } ``` + +Learn more about how to write [accessible components](#/pages/accessibility). diff --git a/src/docs/pages/localizeMoment.md b/src/docs/pages/localizeMoment.md new file mode 100644 index 000000000..259b9bd3a --- /dev/null +++ b/src/docs/pages/localizeMoment.md @@ -0,0 +1,19 @@ +# localizeMoment + +This mixin provides a single helper function to map PKP's application locale codes to those used in [moment.js](https://momentjs.com/). When using moment, you should always use this method to localize the date. + +```js +import moment from 'moment'; +import localizeMoment from '@/mixins/localizeMoment'; + +export default { + mixins: [localizeMoment], + created() { + const timeSince = moment(timestamp) + .locale( + this.getMomentLocale($.pkp.app.currentLocale) + ) + .fromNow(); + } +} +``` diff --git a/src/docs/pages/pages.md b/src/docs/pages/pages.md new file mode 100644 index 000000000..a44d5d69d --- /dev/null +++ b/src/docs/pages/pages.md @@ -0,0 +1,101 @@ +# Pages + +The [Page](#/component/Page) component is the root component of every page in the application. It supports the main navigation menus and implements the default layout of the editorial backend. Every `Page` component can use the utilities and global components without importing them. + +```html +<template> + <pkp-button @click="saveForm"> + {{ __('common.save') }} + <pkp-button> +</template> +``` + +In order to use other components on a page, create a new component that extends the `Page` component. + +```html +<template> + <button-row> + <pkp-button :is-warning="true" @click="cancel"> + {{ __('common.cancel') }} + <pkp-button> + <pkp-button @click="saveForm"> + {{ __('common.save') }} + <pkp-button> + </button-row> +</template> + +<script> +import ButtonRow from '../components/ButtonRow/ButtonRow.vue'; +import Page from '../components/Page/Page.vue'; + +export default { + extends: Page, + components: { + ButtonRow + } +} +</script> +``` + +In practice, `Page` components are usually written without templates. Instead, the application provides the template and the `Page` component is mounted onto the template at run-time. This supports [hydration](https://en.wikipedia.org/wiki/Hydration_(web_development)) and allows plugins to modify the server-side templates. Learn more in the [frontend documentation](https://docs.pkp.sfu.ca/dev/documentation/en/frontend-pages). + +Because the template is not built into the `Page` component, it can be difficult to work with pages in the UI Library. Use a [demo component](#/pages/contributing) to build and test `Page` components in the UI Library with a template. First, create the `Page` component without a template. + +```html +<script> +import ButtonRow from '../components/ButtonRow/ButtonRow.vue'; +import Page from '../components/Page/Page.vue'; + +export default { + name: 'ActionsPage', + extends: Page, + components: { + ButtonRow + }, + methods: { + cancel() { + // ... + }, + saveForm() { + // ... + } + } +} +</script> + +<style lang="less"> +@import '../../styles/_import'; + +.actions__example-button-row { + margin-top: 4rem; +} +``` + +Then create a demo component that uses that `Page` component along with the template and data. + +```html +<template> + <div class="app__page"> + <h1>Example Page Title</h1> + <button-row class="actions__example-button-row"> + <pkp-button :is-warning="true" @click="cancel">Cancel<pkp-button> + <pkp-button @click="saveForm">Save<pkp-button> + </button-row> + </div> +</template> + +<script> +import ActionsPage from '@/components/Container/ActionsPage.vue'; + +export default { + extends: ActionsPage, + data() { + return { + // example data for the root component + } + } +} +</script> +``` + +Learn about how to work with [multilingual data](#/pages/localization). diff --git a/src/docs/pages/usage.md b/src/docs/pages/usage.md deleted file mode 100644 index 7a297db71..000000000 --- a/src/docs/pages/usage.md +++ /dev/null @@ -1,39 +0,0 @@ -# Usage Guide - -This UI Library uses [Vue.js](https://vuejs.org/), a JavaScript library for building interactive applications. If you're not familiar with Vue.js, read its [usage guide](https://vuejs.org/v2/guide/) before continuing. - -## Working with OJS and OMP - -OJS and OMP load this library as a submodule under `/lib/ui-library/`. In `/js/load.js`, each application loads the components it needs from the UI Library. - -When you are using OJS or OMP from a distributed release package, you will only have a pre-compiled `/js/build.js` file. You will not have the full component library under `/lib/ui-library/`. - -If you want to make changes to the components, you will need to use the development repository for [OJS](https://github.com/pkp/ojs) or [OMP](https://github.com/pkp/omp). Follow the steps described there to setup and install the application. - -## Compiling for OJS and OMP - -To compile the `/js/build.js` file while you are working on the library, run the following command from OJS or OMP's root directory. - -```bash -npm run serve -``` - -When you are done, run the following command to compile an optimized `/js/build.js` for production. - -```bash -npm run build -``` - -## Global Vue Instance - -You may want to modify the global Vue object. This may be necessary if you: - -- Add custom components -- Use third-party plugins -- Add global mixins - -You can access the global Vue object at `pkp.Vue`. - -If a Vue plugin tells you it should be registered with `Vue.use(...)`, you can use `pkp.Vue.use(...)`. - -*Your intervention will likely happen before Apps are instantiated and mounted, but after the global event bus is instantiated.* diff --git a/src/docs/utilities/Notify/readme.md b/src/docs/utilities/Notify/readme.md index f436907a0..896599988 100644 --- a/src/docs/utilities/Notify/readme.md +++ b/src/docs/utilities/Notify/readme.md @@ -15,7 +15,7 @@ pkp.eventBus.$emit('notify', 'The submission has been published.', 'success'); Use the `warning` status to indicate an action that has failed or which may need to be corrected by the user. ```js -pkp.eventBus.$emit('notify', 'An ORCID is required for all authors.', 'error'); +pkp.eventBus.$emit('notify', 'An ORCID is required for all authors.', 'warning'); ``` The notifications will disappear after a few seconds, unless the user is hovering their mouse over the notification. Pass the `clear-all-notify` event if you need to clear all the events off the screen immediately. @@ -24,7 +24,7 @@ The notifications will disappear after a few seconds, unless the user is hoverin pkp.eventBus.$emit('clear-all-notify'); ``` -A [Page](#/component/Page) must be present in order to receive the event and display the notification. This should be available on every page in the editorial backend. +A [Page](#/component/Page) must be present in order to receive the event and display the notification. This should be available on [every page in the editorial backend](#/pages/pages). ## Accessibility diff --git a/src/main.js b/src/main.js index 17895fd43..d0ce371e9 100644 --- a/src/main.js +++ b/src/main.js @@ -8,10 +8,13 @@ import VTooltip from 'v-tooltip'; import VueScrollTo from 'vue-scrollto'; import Badge from '@/components/Badge/Badge.vue'; +import Dropdown from '@/components/Dropdown/Dropdown.vue'; import Icon from '@/components/Icon/Icon.vue'; +import Notification from '@/components/Notification/Notification.vue'; import Panel from '@/components/Panel/Panel.vue'; import PanelSection from '@/components/Panel/PanelSection.vue'; import PkpButton from '@/components/Button/Button.vue'; +import PkpHeader from '@/components/Header/Header.vue'; import Spinner from '@/components/Spinner/Spinner.vue'; import Step from '@/components/Steps/Step.vue'; import Steps from '@/components/Steps/Steps.vue'; @@ -33,10 +36,13 @@ Vue.config.productionTip = false; Vue.mixin(GlobalMixins); Vue.component('Badge', Badge); +Vue.component('Dropdown', Dropdown); Vue.component('Icon', Icon); +Vue.component('Notification', Notification); Vue.component('Panel', Panel); Vue.component('PanelSection', PanelSection); Vue.component('PkpButton', PkpButton); +Vue.component('PkpHeader', PkpHeader); Vue.component('Spinner', Spinner); Vue.component('Step', Step); Vue.component('Steps', Steps); diff --git a/src/mixins/autosave.js b/src/mixins/autosave.js index 38cafef07..25c9de22c 100644 --- a/src/mixins/autosave.js +++ b/src/mixins/autosave.js @@ -309,6 +309,9 @@ export default { }, }, watch: { + /** + * Run the reconnect handler when a lost connection is restored + */ isDisconnected(newVal, oldVal) { if (newVal && newVal !== oldVal) { this._runReconnect(); @@ -321,7 +324,7 @@ export default { * in the component using this mixin */ const err = - 'Missing required `{$prop}` prop or callback function. See docs for autosave.js'; + 'Missing required `{$prop}` property or callback function. See docs for autosave.js'; [ 'autosavesKey', 'i18nDiscardChanges', diff --git a/src/mixins/fetch.js b/src/mixins/fetch.js index b175a52fb..e1d737489 100644 --- a/src/mixins/fetch.js +++ b/src/mixins/fetch.js @@ -42,6 +42,7 @@ export default { return { activeFilters: {}, isLoading: false, + itemsMax: 0, latestGetRequest: '', offset: 0, searchPhrase: '', @@ -80,19 +81,19 @@ export default { return; } - var self = this; - this.isLoading = true; // Address issues with multiple async get requests. Store an ID for the // most recent get request. When we receive the response, we // can check that the response matches the most recent get request, and // discard responses that are outdated. - this.latestGetRequest = $.pkp.classes.Helper.uuid(); + const uuid = $.pkp.classes.Helper.uuid(); + this.latestGetRequest = uuid; $.ajax({ url: this.apiUrl, type: 'GET', + context: this, data: { ...this.getParams, ...this.activeFilters, @@ -100,27 +101,26 @@ export default { count: this.count, offset: this.offset, }, - _uuid: this.latestGetRequest, error: function (r) { // Only process latest request response - if (self.latestGetRequest !== this._uuid) { + if (this.latestGetRequest !== uuid) { return; } - self.ajaxErrorCallback(r); + this.ajaxErrorCallback(r); }, success: function (r) { // Only process latest request response - if (self.latestGetRequest !== this._uuid) { + if (this.latestGetRequest !== uuid) { return; } - self.setItems(r.items, r.itemsMax); + this.setItems(r.items, r.itemsMax); }, complete() { // Only process latest request response - if (self.latestGetRequest !== this._uuid) { + if (this.latestGetRequest !== uuid) { return; } - self.isLoading = false; + this.isLoading = false; }, }); }, diff --git a/src/mixins/global.js b/src/mixins/global.js index a0784f774..bdc904af9 100644 --- a/src/mixins/global.js +++ b/src/mixins/global.js @@ -7,11 +7,9 @@ * * @see https://vuejs.org/v2/guide/mixins.html */ -import dialog from './dialog'; import moment from 'moment'; export default { - mixins: [dialog], methods: { /** * Compile a string translation diff --git a/src/mixins/localStorage.js b/src/mixins/localStorage.js index 57555c593..ac1feb8a2 100644 --- a/src/mixins/localStorage.js +++ b/src/mixins/localStorage.js @@ -21,6 +21,9 @@ export default { methods: { /** * Set data in local storage + * + * @param {String} key Any key to store and retrieve data + * @param {mixed} value The data to store */ setLocalStorage(key, value) { localStorage.setItem(key, JSON.stringify(value)); @@ -28,6 +31,8 @@ export default { /** * Get data from local storage + * + * @param {String} key Any key to store and retrieve data */ getLocalStorage(key) { const value = localStorage.getItem(key); @@ -38,6 +43,8 @@ export default { /** * Remove data from local storage + * + * @param {String} key Any key to store and retrieve data */ removeLocaleStorage(key) { localStorage.removeItem(key); @@ -46,7 +53,6 @@ export default { /** * Set the client id from local storage or create * and save a new client id - * */ _setClientId() { const existingClientId = localStorage.getItem('clientId'); @@ -57,7 +63,7 @@ export default { }, /** - * Test if local storage is supported + * Test if local storage is supported by the user's browser * * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API * @return {Boolean} diff --git a/src/router.js b/src/router.js index 46f050f54..c30a72cf1 100644 --- a/src/router.js +++ b/src/router.js @@ -11,10 +11,9 @@ import ComponentChart from './docs/components/Chart/ComponentChart.vue'; import ComponentComposer from './docs/components/Composer/ComponentComposer.vue'; import ComponentDateRange from './docs/components/DateRange/ComponentDateRange.vue'; import ComponentDecisionPage from './docs/components/DecisionPage/ComponentDecisionPage.vue'; -import ComponentDialog from './docs/utilities/Dialog/ComponentDialog.vue'; -import ComponentDoiListPanel from './docs/components/ListPanel/ComponentDoiListPanel'; -import ComponentDoiListPanelOJS from '@/docs/components/ListPanel/ComponentDoiListPanelOJS'; +import ComponentDialog from './docs/mixins/Dialog/ComponentDialog.vue'; import ComponentDropdown from './docs/components/Dropdown/ComponentDropdown.vue'; +import ComponentFetch from './docs/mixins/Fetch/ComponentFetch.vue'; import ComponentFieldAutosuggestPreset from './docs/components/Form/fields/FieldAutosuggestPreset/ComponentFieldAutosuggestPreset.vue'; import ComponentFieldArchivingPn from './docs/components/Form/fields/FieldArchivingPn/ComponentFieldArchivingPn.vue'; import ComponentFieldBaseAutosuggest from './docs/components/Form/fields/FieldBaseAutosuggest/ComponentFieldBaseAutosuggest.vue'; @@ -26,6 +25,7 @@ import ComponentFieldMetadataSetting from './docs/components/Form/fields/FieldMe import ComponentFieldOptions from './docs/components/Form/fields/FieldOptions/ComponentFieldOptions.vue'; import ComponentFieldPreparedContent from './docs/components/Form/fields/FieldPreparedContent/ComponentFieldPreparedContent.vue'; import ComponentFieldRadioInput from './docs/components/Form/fields/FieldRadioInput/ComponentFieldRadioInput.vue'; +import ComponentFieldRichText from './docs/components/Form/fields/FieldRichText/ComponentFieldRichText.vue'; import ComponentFieldRichTextarea from './docs/components/Form/fields/FieldRichTextarea/ComponentFieldRichTextarea.vue'; import ComponentFieldSelect from './docs/components/Form/fields/FieldSelect/ComponentFieldSelect.vue'; import ComponentFieldSelectIssue from './docs/components/Form/fields/FieldSelectIssue/ComponentFieldSelectIssue.vue'; @@ -187,6 +187,11 @@ export default new Router({ name: 'Form/fields/FieldRadioInput', component: ComponentFieldRadioInput, }, + { + path: '/component/Form/fields/FieldRichText/:example?', + name: 'Form/fields/FieldRichText', + component: ComponentFieldRichText, + }, { path: '/component/Form/fields/FieldRichTextarea/:example?', name: 'Form/fields/FieldRichTextarea', @@ -292,16 +297,6 @@ export default new Router({ name: 'CatalogListPanel', component: ComponentCatalogListPanel, }, - { - path: '/component/ListPanel/components/DoiListPanel/:example?', - name: 'DoiListPanel', - component: ComponentDoiListPanel, - }, - { - path: '/component/ListPanel/components/DoiListPanelOJS/:example?', - name: 'DoiListPanelOJS', - component: ComponentDoiListPanelOJS, - }, { path: '/component/ListPanel/components/InstitutionsListPanel/:example?', name: 'InstitutionsListPanel', @@ -413,10 +408,15 @@ export default new Router({ component: ComponentWorkflowPage, }, { - path: '/utilities/Dialog/:example?', + path: '/mixins/dialog/:example?', name: 'Dialog', component: ComponentDialog, }, + { + path: '/mixins/fetch/:example?', + name: 'Fetch', + component: ComponentFetch, + }, { path: '/utilities/Notify/:example?', name: 'Notify',