├── .eslintignore ├── .forceignore ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── config └── project-scratch-def.json ├── force-app └── main │ └── default │ ├── classes │ ├── FileCollection.cls │ ├── FileCollection.cls-meta.xml │ ├── FileOperationsController.cls │ └── FileOperationsController.cls-meta.xml │ └── lwc │ ├── .eslintrc.json │ └── flowFileUpload │ ├── flowFileUpload.css │ ├── flowFileUpload.html │ ├── flowFileUpload.js │ ├── flowFileUpload.js-meta.xml │ └── helpers │ └── helpers.js ├── package-lock.json ├── package.json └── sfdx-project.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lwc/**/*.css 2 | **/lwc/**/*.html 3 | **/lwc/**/*.json 4 | **/lwc/**/*.svg 5 | **/lwc/**/*.xml 6 | .sfdx -------------------------------------------------------------------------------- /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sfdx/ 7 | .localdevserver/ 8 | 9 | # LWC VSCode autocomplete 10 | **/lwc/jsconfig.json 11 | 12 | # LWC Jest coverage reports 13 | coverage/ 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # Eslint cache 26 | .eslintcache 27 | 28 | # MacOS system files 29 | .DS_Store 30 | 31 | # Windows system files 32 | Thumbs.db 33 | ehthumbs.db 34 | [Dd]esktop.ini 35 | $RECYCLE.BIN/ 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | .localdevserver 6 | .sfdx 7 | 8 | coverage/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "overrides": [ 4 | { 5 | "files": "**/lwc/**/*.html", 6 | "options": { "parser": "lwc" } 7 | }, 8 | { 9 | "files": "*.{cmp,page,component}", 10 | "options": { "parser": "html" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Suraj Pillai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce App 2 | 3 | This is an LWC for uploading files that tries to mimick https://github.com/Yuvaleros/material-ui-dropzone. It supports a limited set of features, comparitively speaking, and has been primarily designed to be used in flow screens. The accompanying invocable method lets you create a content document. content version and content document link, as needed. 4 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "demo company", 3 | "edition": "Developer", 4 | "features": [] 5 | } 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FileCollection.cls: -------------------------------------------------------------------------------- 1 | public with sharing class FileCollection { 2 | @InvocableVariable 3 | public ContentVersion[] files; 4 | 5 | @InvocableVariable 6 | public Id relatedRecordId; 7 | } 8 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FileCollection.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FileOperationsController.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description contains operations realted to file upload lwc 3 | */ 4 | public with sharing class FileOperationsController { 5 | @InvocableMethod( 6 | label='Create Content' 7 | description='Create Content Document/Version and link it with a record' 8 | ) 9 | public static List createContentDocumentLink( 10 | List fileCollectionList 11 | ) { 12 | List versionsToLink = new List(); 13 | ContentVersion[] versionsToCreate = new List{}; 14 | List versionRecordsToLink = new List(); 15 | 16 | for (FileCollection fc : fileCollectionList) { 17 | for (ContentVersion cv : fc.files) { 18 | if (cv.ContentDocumentId == null && fc.relatedRecordId != null) { 19 | versionsToLink.add(new LinkWrapper(cv, fc.relatedRecordId)); 20 | versionRecordsToLink.add(cv); 21 | } 22 | versionsToCreate.add(cv); 23 | } 24 | } 25 | if (versionsToCreate.size() > 0) { 26 | insert versionsToCreate; 27 | } 28 | ContentDocumentLink[] links = new List{}; 29 | Set documentIdsAdded = new Set(); 30 | Map cvMap = new Map( 31 | [ 32 | SELECT ContentDocumentId 33 | FROM ContentVersion 34 | WHERE Id = :versionRecordsToLink 35 | ] 36 | ); 37 | for (LinkWrapper lw : versionsToLink) { 38 | ContentDocumentLink link = new ContentDocumentLink(); 39 | link.ContentDocumentId = cvMap.get(lw.cv.Id).ContentDocumentId; 40 | link.LinkedEntityId = lw.relatedRecordId; 41 | if (!documentIdsAdded.contains(link.ContentDocumentId)) { 42 | links.add(link); 43 | documentIdsAdded.add(link.ContentDocumentId); 44 | } 45 | } 46 | if (links.size() > 0) { 47 | insert links; 48 | } 49 | return new List(new Map(versionsToCreate).keySet()); 50 | } 51 | 52 | private class LinkWrapper { 53 | public ContentVersion cv; 54 | public Id relatedRecordId; 55 | public LinkWrapper(ContentVersion cv, Id relatedRecordId) { 56 | this.cv = cv; 57 | this.relatedRecordId = relatedRecordId; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FileOperationsController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/flowFileUpload/flowFileUpload.css: -------------------------------------------------------------------------------- 1 | @keyframes progress { 2 | 0% { 3 | background-position: 0 0; 4 | } 5 | 100% { 6 | background-position: -70px 0; 7 | } 8 | } 9 | .dropzone { 10 | position: relative; 11 | width: 100%; 12 | min-height: 250px; 13 | background-color: #f0f0f0; 14 | border: dashed; 15 | border-color: #c8c8c8; 16 | cursor: pointer; 17 | box-sizing: border-box; 18 | } 19 | .stripes { 20 | border: solid; 21 | background-image: repeating-linear-gradient( 22 | -45deg, 23 | #f0f0f0, 24 | #f0f0f0 25px, 25 | #c8c8c8 25px, 26 | #c8c8c8 50px 27 | ); 28 | animation: progress 2s linear infinite !important; 29 | background-size: 150% 100%; 30 | } 31 | .rejectstripes { 32 | border: solid; 33 | background-image: repeating-linear-gradient( 34 | -45deg, 35 | #fc8785, 36 | #fc8785 25px, 37 | #f4231f 25px, 38 | #f4231f 50px 39 | ); 40 | animation: progress 2s linear infinite !important; 41 | background-size: 150% 100%; 42 | } 43 | .dropzonetextstyle { 44 | text-align: center; 45 | } 46 | .uploadiconsize { 47 | width: 51; 48 | height: 51; 49 | color: #909090; 50 | } 51 | .dropzoneparagraph { 52 | font-size: 24; 53 | } 54 | .message { 55 | font: 15px bold; 56 | margin-top: 8px; 57 | } 58 | .success { 59 | color: green; 60 | } 61 | .error { 62 | color: red; 63 | } 64 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/flowFileUpload/flowFileUpload.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/flowFileUpload/flowFileUpload.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from "lwc"; 2 | import { createFileFromUrl, accepts } from "./helpers/helpers.js"; 3 | import { createRecord } from "lightning/uiRecordApi"; 4 | import { ShowToastEvent } from "lightning/platformShowToastEvent"; 5 | import { FlowAttributeChangeEvent } from "lightning/flowSupport"; 6 | 7 | const BASE64REGEXP = new RegExp(/^data(.*)base64,/); 8 | 9 | export default class FlowFileUpload extends LightningElement { 10 | fileObjects = []; 11 | openSnackbar = false; 12 | snackbarMessage = ""; 13 | snackbarVariant = "success"; 14 | @api relatedRecordId = "0010t00001NKoQrAAL"; 15 | @api contentDocumentId; 16 | @api multiple = false; 17 | @api acceptedFileTypes = ["image/png", "application/pdf"]; 18 | @api dropzoneText = ""; 19 | @api initialFiles = []; 20 | @api filesLimit = 1; 21 | @api maxFileSize = 2048; 22 | dropRejected = false; 23 | _isDragActive = false; 24 | 25 | @api 26 | get contentVersions() { 27 | return this.fileObjects.map(f => { 28 | return this._createPayload(f); 29 | }); 30 | } 31 | 32 | get messageClassName() { 33 | return "message " + (this.dropRejected ? "error" : "success"); 34 | } 35 | 36 | getFileLimitExceedMessage(filesLimit) { 37 | return `Maximum allowed number of files exceeded. Only ${filesLimit} allowed`; 38 | } 39 | 40 | getFileAddedMessage(fileName) { 41 | return `File ${fileName} successfully added.`; 42 | } 43 | 44 | getFileRemovedMessage(fileName) { 45 | return `File ${fileName} removed.`; 46 | } 47 | 48 | getDropRejectMessage(rejectedFile, acceptedFiles, maxFileSize) { 49 | let message = `File ${rejectedFile.name} was rejected. `; 50 | if (!acceptedFiles.includes(rejectedFile.type)) { 51 | message += "File type not supported. "; 52 | } 53 | if (rejectedFile.size > maxFileSize) { 54 | message += "File is too big. Size limit is " + maxFileSize + ". "; 55 | } 56 | return message; 57 | } 58 | 59 | connectedCallback() { 60 | this.filesArray(this.initialFiles); 61 | } 62 | 63 | async filesArray(urls) { 64 | try { 65 | for (const url of urls) { 66 | /*eslint-disable-next-line no-await-in-loop*/ 67 | const file = await createFileFromUrl(url); 68 | const reader = new FileReader(); 69 | reader.onload = () => { 70 | this.fileObjects = this.fileObjects.concat({ 71 | file: file, 72 | data: reader.result 73 | }); 74 | }; 75 | reader.readAsDataURL(file); 76 | } 77 | } catch (err) { 78 | console.log(err); 79 | } 80 | } 81 | 82 | _containsFiles(event) { 83 | if (event.dataTransfer && event.dataTransfer.types) { 84 | return Array.from(event.dataTransfer.types).indexOf("Files") > -1; 85 | } 86 | return false; 87 | } 88 | 89 | handleDrag(event) { 90 | event.preventDefault(); 91 | //this._isDragActive = true; 92 | 93 | if (event.dataTransfer) { 94 | try { 95 | event.dataTransfer.dropEffect = "copy"; 96 | } catch (e) {} /* eslint-disable-line no-empty */ 97 | } 98 | } 99 | 100 | handleSubmit() { 101 | this.fileObjects.forEach(f => { 102 | let payload = { 103 | apiName: "ContentVersion", 104 | fields: this._createPayload(f) 105 | }; 106 | createRecord(payload) 107 | .then(() => { 108 | this.dispatchEvent( 109 | new ShowToastEvent({ 110 | variant: "success", 111 | message: "File " + f.file.name + " uploaded successfully" 112 | }) 113 | ); 114 | return Promise.resolve(1); 115 | }) 116 | .then(() => { 117 | if (!this.contentDocumentId && this.relatedRecordId) { 118 | this.dispatchEvent( 119 | new ShowToastEvent({ 120 | variant: "success", 121 | message: `Content Document Link created` 122 | }) 123 | ); 124 | } 125 | }) 126 | .catch(err => { 127 | this.dispatchEvent( 128 | new ShowToastEvent({ 129 | variant: "error", 130 | message: `An error occurred with file ${f.file.name}: ${err.body.message}` 131 | }) 132 | ); 133 | }); 134 | }); 135 | } 136 | 137 | _createPayload(fileObject) { 138 | const fileData = { 139 | Title: fileObject.file.name, 140 | PathOnClient: fileObject.file.name, 141 | VersionData: fileObject.data.replace(BASE64REGEXP, "") 142 | }; 143 | if (this.contentDocumentId) { 144 | fileData.ContentDocumentId = this.contentDocumentId; 145 | } 146 | return fileData; 147 | } 148 | 149 | _validate(files) { 150 | const acceptedFiles = []; 151 | const rejectedFiles = []; 152 | 153 | files.forEach(file => { 154 | if (accepts(file, this.acceptedFileTypes)) { 155 | acceptedFiles.push(file); 156 | } else { 157 | rejectedFiles.push(file); 158 | } 159 | }); 160 | 161 | if (!this.multiple && acceptedFiles.length > 1) { 162 | rejectedFiles.push(...acceptedFiles.splice(0)); // Reject everything and empty accepted files 163 | } 164 | return { acceptedFiles, rejectedFiles }; 165 | } 166 | 167 | get classNames() { 168 | return `dropzone ${this._isDragActive && 169 | (this.dropRejected ? "rejectstripes" : "stripes")}`; 170 | } 171 | 172 | _getFilesFromEvent(event) { 173 | return Array.from(event.dataTransfer.items) 174 | .filter(i => i.kind === "file") 175 | .map(i => i.getAsFile()); 176 | } 177 | 178 | handleDragEnter(event) { 179 | this._isDragActive = true; 180 | if (!this._containsFiles(event)) { 181 | this.dropRejected = true; 182 | return false; 183 | } 184 | this.dropRejected = false; 185 | return true; 186 | } 187 | 188 | handleDragLeave() { 189 | this._isDragActive = false; 190 | } 191 | 192 | handleDrop(event) { 193 | //{{{ 194 | //const _this = this; 195 | this._isDragActive = false; 196 | let files = this._getFilesFromEvent(event); 197 | event.preventDefault(); 198 | if ( 199 | this.filesLimit > 1 && 200 | this.fileObjects.length + files.length > this.filesLimit 201 | ) { 202 | this.openSnackBar = true; 203 | this.snackbarMessage = this.getFileLimitExceedMessage(this.filesLimit); 204 | this.snackbarVariant = "error"; 205 | this.dropRejected = true; 206 | } else { 207 | let results = this._validate(files); 208 | if (results.rejectedFiles.length > 0) { 209 | return this.handleDropRejected(results.rejectedFiles, event); 210 | } 211 | this.dropRejected = false; 212 | let count = 0; 213 | let message = ""; 214 | if (!Array.isArray(files)) files = [files]; 215 | 216 | files.forEach(file => { 217 | const reader = new FileReader(); 218 | reader.onload = () => { 219 | this.fileObjects = 220 | this.filesLimit <= 1 221 | ? [ 222 | { 223 | file: file, 224 | data: reader.result 225 | } 226 | ] 227 | : this.fileObjects.concat({ 228 | file: file, 229 | data: reader.result 230 | }); 231 | /** 232 | if (this.onChange) { 233 | this.onChange(_this.state.fileObjects.map((fileObject) => fileObject.file)); 234 | } 235 | if (this.onDrop) { 236 | this.onDrop(file); 237 | } **/ 238 | message += this.getFileAddedMessage(file.name); 239 | count++; // we cannot rely on the index because this is asynchronous 240 | if (count === files.length) { 241 | // display message when the last one fires 242 | this.openSnackBar = true; 243 | this.snackbarMessage = message; 244 | this.snackbarVariant = "success"; 245 | this._fireAttributeChangeEvent(); 246 | } 247 | }; 248 | reader.readAsDataURL(file); 249 | }); 250 | } 251 | return null; 252 | } //}}} 253 | 254 | _fireAttributeChangeEvent() { 255 | const attributeChangeEvent = new FlowAttributeChangeEvent( 256 | "contentVersions", 257 | this.contentVersions 258 | ); 259 | this.dispatchEvent(attributeChangeEvent); 260 | } 261 | 262 | handleRemove = fileIndex => event => { 263 | event.stopPropagation(); 264 | const fileObjects = this.fileObjects; 265 | const file = fileObjects.filter((fileObject, i) => { 266 | return i === fileIndex; 267 | })[0].file; 268 | fileObjects.splice(fileIndex, 1); 269 | this.fileObjects = fileObjects; 270 | if (this.onDelete) { 271 | this.onDelete(file); 272 | } 273 | if (this.onChange) { 274 | this.onChange(this.state.fileObjects.map(fileObject => fileObject.file)); 275 | } 276 | this.openSnackBar = true; 277 | this.snackbarMessage = this.getFileRemovedMessage(file.name); 278 | this.snackbarVariant = "info"; 279 | }; 280 | 281 | handleDropRejected(rejectedFiles, evt) { 282 | this.dropRejected = true; 283 | let message = ""; 284 | rejectedFiles.forEach(rejectedFile => { 285 | message += this.getDropRejectMessage( 286 | rejectedFile, 287 | this.acceptedFileTypes, 288 | this.maxFileSize 289 | ); 290 | }); 291 | if (this.onDropRejected) { 292 | this.onDropRejected(rejectedFiles, evt); 293 | } 294 | this.openSnackBar = true; 295 | this.snackbarMessage = message; 296 | this.snackbarVariant = "error"; 297 | } 298 | 299 | handleCloseSnackbar = () => { 300 | this.openSnackBar = false; 301 | }; 302 | } 303 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/flowFileUpload/flowFileUpload.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48.0 4 | true 5 | 6 | lightning__HomePage 7 | lightning__FlowScreen 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/flowFileUpload/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | function accepts(file, acceptedFiles) { 2 | if (file && acceptedFiles) { 3 | const acceptedFilesArray = Array.isArray(acceptedFiles) 4 | ? acceptedFiles 5 | : acceptedFiles.split(","); 6 | const fileName = file.name || ""; 7 | const mimeType = file.type || ""; 8 | const baseMimeType = mimeType.replace(/\/.*$/, ""); 9 | 10 | return acceptedFilesArray.some((type) => { 11 | const validType = type.trim(); 12 | if (validType.charAt(0) === ".") { 13 | return fileName.toLowerCase().endsWith(validType.toLowerCase()); 14 | } else if (validType.endsWith("/*")) { 15 | // This is something like a image/* mime type 16 | return baseMimeType === validType.replace(/\/.*$/, ""); 17 | } 18 | return mimeType === validType; 19 | }); 20 | } 21 | return true; 22 | } 23 | 24 | function isImage(file) { 25 | if (file.type.split("/")[0] === "image") { 26 | return true; 27 | } 28 | } 29 | function convertBytesToMbsOrKbs(filesize) { 30 | let size = ""; 31 | // I know, not technically correct... 32 | if (filesize >= 1000000) { 33 | size = filesize / 1000000 + " megabytes"; 34 | } else if (filesize >= 1000) { 35 | size = filesize / 1000 + " kilobytes"; 36 | } else { 37 | size = filesize + " bytes"; 38 | } 39 | return size; 40 | } 41 | 42 | async function createFileFromUrl(url) { 43 | const response = await fetch(url); 44 | const data = await response.blob(); 45 | const metadata = { type: data.type }; 46 | const filename = url.replace(/\?.+/, "").split("/").pop(); 47 | const ext = data.type.split("/").pop(); 48 | return new File([data], `${filename}.${ext}`, metadata); 49 | } 50 | export { createFileFromUrl, accepts }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salesforce-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Salesforce App", 6 | "scripts": { 7 | "lint": "npm run lint:lwc", 8 | "lint:lwc": "eslint force-app/main/default/lwc", 9 | "test": "npm run test:unit", 10 | "test:unit": "sfdx-lwc-jest", 11 | "test:unit:watch": "sfdx-lwc-jest --watch", 12 | "test:unit:debug": "sfdx-lwc-jest --debug", 13 | "test:unit:coverage": "sfdx-lwc-jest --coverage", 14 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 15 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"" 16 | }, 17 | "devDependencies": { 18 | "@prettier/plugin-xml": "^0.7.0", 19 | "@salesforce/eslint-config-lwc": "^0.4.0", 20 | "@salesforce/sfdx-lwc-jest": "^0.7.0", 21 | "eslint": "^5.16.0", 22 | "prettier": "^1.19.1", 23 | "prettier-plugin-apex": "^1.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "namespace": "", 9 | "sfdcLoginUrl": "https://login.salesforce.com", 10 | "sourceApiVersion": "48.0" 11 | } --------------------------------------------------------------------------------