2018年11月18日 星期日
Implement algolia autocomplete with rails
```js
// app/javascripts/application.js
const algoliasearch = require('algoliasearch/lite')
const autocomplete = require('autocomplete.js')
const client = algoliasearch(process.env.ALGOLIA_APPLICATION_ID, process.env.ALGOLIA_SEARCH_ONLY_API_KEY)
const pack = {
algoliaClient: client,
autocomplete: autocomplete
}
window.Pack = pack
```
```erb
<!-- Algolia search autocomplete HTML markup -->
<div class="aa-input-container" id="aa-input-container">
<input type="search" id="aa-search-input" class="aa-input-search" placeholder="Search multiple fields in one place..." name="search" autocomplete="off" />
<svg class="aa-input-icon" viewBox="654 -372 1664 1664">
<path d="M1806,332c0-123.3-43.8-228.8-131.5-316.5C1586.8-72.2,1481.3-116,1358-116s-228.8,43.8-316.5,131.5 C953.8,103.2,910,208.7,910,332s43.8,228.8,131.5,316.5C1129.2,736.2,1234.7,780,1358,780s228.8-43.8,316.5-131.5 C1762.2,560.8,1806,455.3,1806,332z M2318,1164c0,34.7-12.7,64.7-38,90s-55.3,38-90,38c-36,0-66-12.7-90-38l-343-342 c-119.3,82.7-252.3,124-399,124c-95.3,0-186.5-18.5-273.5-55.5s-162-87-225-150s-113-138-150-225S654,427.3,654,332 s18.5-186.5,55.5-273.5s87-162,150-225s138-113,225-150S1262.7-372,1358-372s186.5,18.5,273.5,55.5s162,87,225,150s113,138,150,225 S2062,236.7,2062,332c0,146.7-41.3,279.7-124,399l343,343C2305.7,1098.7,2318,1128.7,2318,1164z" />
</svg>
</div>
<!-- Initialize autocomplete menu -->
<script>
var index = Pack.algoliaClient.initIndex('<%= index_name %>');
Pack.autocomplete('#aa-search-input',
{ hint: false }, {
source: Pack.autocomplete.sources.hits(index, {hitsPerPage: 5, filters: 'discarded_at_i < 1'}),
displayKey: 'name',
templates: {
suggestion: function(suggestion) {
return `<a href='<%= path_prefix %>/${suggestion.objectID}'>${suggestion._highlightResult.name.value}</a>`
}
}
}).on("autocomplete:selected", function (event, suggestion, dataset) {
window.location.href = `<%= path_prefix %>/${suggestion.objectID}`;
})
</script>
```
2018年11月17日 星期六
Submit a rails ActiveStorage issue
https://github.com/rails/rails/issues/34470
### Steps to reproduce
When passing invalid file object to initialize DirectUpload instance, it will fail silently while creating because FileChecksum fail to acknowledge it's a invalid object.
### Expected behavior
I expect when I passing a invalid file object to initialize DirectUpload instance to create upload, it should show error message.
```js
// This result is what I got from UppyJS, it doesn't contains real file data but it still provide meta data like file mimeType, name...etc
const result = {
"source": "GoogleDrive",
"id": "uppy-kittyjpg-image/jpeg",
"name": "kitty.jpg",
"extension": "jpg",
"meta": {
"name": "kitty.jpg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://drive-thirdparty.googleusercontent.com/16/type/image/jpeg",
"name": "kitty.jpg",
"mimeType": "image/jpeg",
"id": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"thumbnail": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"requestPath": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"modifiedDate": "2018-11-07T15:06:35.239Z",
"custom": {
"isTeamDrive": false
}
},
"progress": {
"percentage": 0,
"bytesUploaded": 0,
"bytesTotal": 0,
"uploadComplete": false,
"uploadStarted": false
},
"size": 0,
"isRemote": true,
"remote": {
"serverUrl": "http://localhost:3020",
"url": "http://localhost:3020/drive/get/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"body": {
"fileId": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
},
"providerOptions": {
"serverUrl": "http://localhost:3020",
"provider": "drive",
"authProvider": "google"
}
},
"preview": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
}
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Should print something
```
### Actual behavior
```js
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Nothing printed
```
### Digging into it
The reason there are no error is because `rails/activestorage/app/javascript/activestorage/file_checksum.js`, in `create` function it only add error listener for fileReader, however, when passing invalid file object into FileChecksum, in this case, in line 14, `this.file.size` is `undefined` so `this.chunkCount` will be `NaN` at the first place, it never had chance to raise error inside fileReader.
According to https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js#L42
It will just return false and invoke no callback.
I don't know if it's designed by purpose or maybe we can add some check to make sure file object is valid? It might not be an issue if you know what you're doing very well, however, it did take me a while to investigate it while I was trying to integrate Uppy with ActiveStorage, so I think it's worth to mention or add checking mechanism into codebase. Any thought? I'm happy to send a PR for this if you think it's worth to check.
### System configuration
**Rails version**:
5.2.1
**Ruby version**:
2.5.1
**ActiveStorage npm package version**
5.2.1
### Steps to reproduce
When passing invalid file object to initialize DirectUpload instance, it will fail silently while creating because FileChecksum fail to acknowledge it's a invalid object.
### Expected behavior
I expect when I passing a invalid file object to initialize DirectUpload instance to create upload, it should show error message.
```js
// This result is what I got from UppyJS, it doesn't contains real file data but it still provide meta data like file mimeType, name...etc
const result = {
"source": "GoogleDrive",
"id": "uppy-kittyjpg-image/jpeg",
"name": "kitty.jpg",
"extension": "jpg",
"meta": {
"name": "kitty.jpg",
"type": "image/jpeg"
},
"type": "image/jpeg",
"data": {
"isFolder": false,
"icon": "https://drive-thirdparty.googleusercontent.com/16/type/image/jpeg",
"name": "kitty.jpg",
"mimeType": "image/jpeg",
"id": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"thumbnail": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"requestPath": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"modifiedDate": "2018-11-07T15:06:35.239Z",
"custom": {
"isTeamDrive": false
}
},
"progress": {
"percentage": 0,
"bytesUploaded": 0,
"bytesTotal": 0,
"uploadComplete": false,
"uploadStarted": false
},
"size": 0,
"isRemote": true,
"remote": {
"serverUrl": "http://localhost:3020",
"url": "http://localhost:3020/drive/get/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU",
"body": {
"fileId": "1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
},
"providerOptions": {
"serverUrl": "http://localhost:3020",
"provider": "drive",
"authProvider": "google"
}
},
"preview": "http://localhost:3020/drive/thumbnail/1Q61JlL7ojCoV3zqZCWFOTaJe054LJ_GU"
}
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Should print something
```
### Actual behavior
```js
const uploadFile = (file) {
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
console.log("Error!!")
} else {
console.log("No Error")
}
})
}
uploadFile(result.data) // => Nothing printed
```
### Digging into it
The reason there are no error is because `rails/activestorage/app/javascript/activestorage/file_checksum.js`, in `create` function it only add error listener for fileReader, however, when passing invalid file object into FileChecksum, in this case, in line 14, `this.file.size` is `undefined` so `this.chunkCount` will be `NaN` at the first place, it never had chance to raise error inside fileReader.
According to https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js#L42
It will just return false and invoke no callback.
I don't know if it's designed by purpose or maybe we can add some check to make sure file object is valid? It might not be an issue if you know what you're doing very well, however, it did take me a while to investigate it while I was trying to integrate Uppy with ActiveStorage, so I think it's worth to mention or add checking mechanism into codebase. Any thought? I'm happy to send a PR for this if you think it's worth to check.
### System configuration
**Rails version**:
5.2.1
**Ruby version**:
2.5.1
**ActiveStorage npm package version**
5.2.1
Integrate Uppy with Rails and ActiveStorage and use google drive direct upload
We're going to integrate Uppy with "upload from Google Drive" feature with rails backend and ActiveStorage, in this case we are going to upload multiple images at once and using the direct upload concept.
What we need?
1. Install Uppy JS into rails using webpacker
- Figure out how direct upload works with ActiveStorage
- A companion server (a proxy server to handle google auth)
## 1. Install Uppy JS into rails using webpacker
```json
// package.json
{
"name": "platform",
"private": true,
"dependencies": {
"@rails/webpacker": "3.5",
"dotenv": "^6.1.0",
"uppy": "^0.28.0"
},
"devDependencies": {
"webpack-dev-server": "2.11.2"
}
}
```
To hide credential from git history, we add `dotenv` npm package to do so
```js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')
const dotenvFiles = [
`.env.${process.env.NODE_ENV}.local`,
'.env.local',
`.env.${process.env.NODE_ENV}`,
'.env'
]
dotenvFiles.forEach((dotenvFile) => {
dotenv.config({ path: dotenvFile, silent: true })
})
environment.plugins.prepend('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))
module.exports = environment
```
```
# .env
APPLICATION_URL=http://localhost:3001
UPPY_COMPANION_SERVER_URL=http://localhost:3020
```
```js
// app/javascripts/uppy.js
const Uppy = require("@uppy/core")
const Dashboard = require("@uppy/dashboard")
const XHRUpload = require("@uppy/xhr-upload")
const GoogleDrive = require("@uppy/google-drive")
const input = document.querySelector("input[type=file]")
const form = document.querySelector("form")
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
const uppyTrigger = "#js-uppy-upload"
const uppyTriggerElement = document.querySelector(uppyTrigger)
const startUppy = () => {
const uppy = Uppy({
debug: true,
restrictions: {
allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif']
},
})
.use(Dashboard, { trigger: uppyTrigger })
.use(GoogleDrive, {
target: Dashboard,
serverUrl: process.env.UPPY_COMPANION_SERVER_URL || "http://localhost:3020"
})
.use(XHRUpload, {
endpoint: `${process.env.APPLICATION_URL}/uploader/image`,
bundle: false,
headers: { "X-CSRF-Token": csrfToken }
})
uppy.on("upload-success", (file, body) => {
uppy.setFileState(file.id, {
xhr: Object.assign({}, file, {
uploadURL: body["uploadURL"],
signedId: body["signedId"]
})
})
})
uppy.on("complete", result => {
result.successful.forEach(file => {
insertImageSignedId(form, input, file.xhr.signedId)
})
})
}
const insertImageSignedId = (form, input, signed_id) => {
const hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("value", signed_id)
hiddenField.name = input.name
form.appendChild(hiddenField)
}
if (uppyTriggerElement) {
startUppy()
}
```
And add uppy.js into layout
```erb
<!DOCTYPE html>
<html>
<head>
<title>Uppy Example</title>
<%= stylesheet_link_tag 'admin/default', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
<%= csrf_meta_tags %>
</head>
<body>
<div class="container">
<%= yield %>
<%= javascript_pack_tag 'uppy' %>
</div>
</body>
</html>
```
form example
```erb
<div class="form-group">
<%= f.label "Images" %>
<div id="js-uppy-upload" class="form-control">
Upload
</div>
<%= %>
<%= f.file_field :images, multiple: true, class: "hidden" %>
</div>
```
## 2. Figure out how direct upload works with ActiveStorage
Add a new endpoint into your rails project, this simplest implementation is below:
```rb
class UploaderController < ApplicationController
# TODO: Add CSRF protection
skip_before_action :verify_authenticity_token
# TODO: Add authentication, cookie based authentication should be good enough
def image
# XHRUpload will send image 1 by 1
# https://uppy.io/docs/xhr-upload/#bundle-false
file = params[:files].first
blob = ActiveStorage::Blob.create_after_upload!(
io: file,
filename: file.original_filename,
content_type: file.content_type
)
render json: { uploadURL: url_for(blob), signedId: blob.signed_id }
end
end
```
Cons here is that we still need to process image file in our server, I can't find a way to avoid it with ActiveStorage, if you know a better way I'm happy to hear, thanks :)
Now the upload workflow will be
User select files from google drive -> Client tell companion server to upload file to our API -> API server process image, save it to AWS S3 and return ActiveStorage's record's (blob) signed_id -> Client add signed_id into form field and submit with it -> Rails server bind uploaded image with form record
## 3. A companion server (a proxy server to handle google auth)
I use the heroku node template, so there are heroku cli dependency (to load .env file), you can remove this dependency simply by loading `.env` yourself
```json
// package.json
{
"name": "example-uppy-companion-server",
"version": "1.0.0",
"description": "Example uppy companion server",
"engines": {
"node": "10.x"
},
"main": "index.js",
"scripts": {
"start": "heroku local"
},
"dependencies": {
"@uppy/companion": "^0.15.0",
"body-parser": "^1.18.3",
"express": "^4.16.4",
"express-session": "^1.15.6"
},
"devDependencies": {
"request": "^2.81.0",
"tape": "^4.7.0"
}
}
```
```js
// index.js
const express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')
const app = express()
app.use(bodyParser.json())
app.use(session({
secret: 'some-secret',
resave: true,
saveUninitialized: true
}))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
res.setHeader('Access-Control-Allow-Credentials', 'true')
next()
})
// Routes
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.send('Welcome to Companion')
})
// initialize uppy
const uppyOptions = {
providerOptions: {
google: {
key: process.env.PROVIDER_GOOGLE_DRIVE_KEY || "",
secret: process.env.PROVIDER_GOOGLE_DRIVE_SECRET || 5
}
},
server: {
host: process.env.PROVIDER_SERVER_HOST || "localhost:3020",
protocol: process.env.PROVIDER_SERVER_PROTOCOL || "http"
},
filePath: "./output",
secret: "some-secret",
debug: true
};
app.use(companion.app(uppyOptions))
// handle 404
app.use((req, res, next) => {
return res.status(404).json({ message: 'Not Found' })
})
// handle server errors
app.use((err, req, res, next) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
})
companion.socket(app.listen(process.env.PORT || 3020), uppyOptions)
console.log('Welcome to Companion!')
```
```
# .env
PROVIDER_SERVER_HOST=localhost:3020
PROVIDER_SERVER_PROTOCOL=http
PROVIDER_GOOGLE_DRIVE_KEY=PROVIDER_GOOGLE_DRIVE_KEY
PROVIDER_GOOGLE_DRIVE_SECRET=PROVIDER_GOOGLE_DRIVE_SECRET
PORT=3020
```
https://stackoverflow.com/questions/50872912/integration-rails-active-storage-with-js-uploaders
https://github.com/rails/rails/issues/32251
https://medium.com/weareevermore/manual-uploads-using-activestorage-47808dab1b65
What we need?
1. Install Uppy JS into rails using webpacker
- Figure out how direct upload works with ActiveStorage
- A companion server (a proxy server to handle google auth)
## 1. Install Uppy JS into rails using webpacker
```json
// package.json
{
"name": "platform",
"private": true,
"dependencies": {
"@rails/webpacker": "3.5",
"dotenv": "^6.1.0",
"uppy": "^0.28.0"
},
"devDependencies": {
"webpack-dev-server": "2.11.2"
}
}
```
To hide credential from git history, we add `dotenv` npm package to do so
```js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')
const dotenvFiles = [
`.env.${process.env.NODE_ENV}.local`,
'.env.local',
`.env.${process.env.NODE_ENV}`,
'.env'
]
dotenvFiles.forEach((dotenvFile) => {
dotenv.config({ path: dotenvFile, silent: true })
})
environment.plugins.prepend('Environment', new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(process.env))))
module.exports = environment
```
```
# .env
APPLICATION_URL=http://localhost:3001
UPPY_COMPANION_SERVER_URL=http://localhost:3020
```
```js
// app/javascripts/uppy.js
const Uppy = require("@uppy/core")
const Dashboard = require("@uppy/dashboard")
const XHRUpload = require("@uppy/xhr-upload")
const GoogleDrive = require("@uppy/google-drive")
const input = document.querySelector("input[type=file]")
const form = document.querySelector("form")
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
const uppyTrigger = "#js-uppy-upload"
const uppyTriggerElement = document.querySelector(uppyTrigger)
const startUppy = () => {
const uppy = Uppy({
debug: true,
restrictions: {
allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png', '.gif']
},
})
.use(Dashboard, { trigger: uppyTrigger })
.use(GoogleDrive, {
target: Dashboard,
serverUrl: process.env.UPPY_COMPANION_SERVER_URL || "http://localhost:3020"
})
.use(XHRUpload, {
endpoint: `${process.env.APPLICATION_URL}/uploader/image`,
bundle: false,
headers: { "X-CSRF-Token": csrfToken }
})
uppy.on("upload-success", (file, body) => {
uppy.setFileState(file.id, {
xhr: Object.assign({}, file, {
uploadURL: body["uploadURL"],
signedId: body["signedId"]
})
})
})
uppy.on("complete", result => {
result.successful.forEach(file => {
insertImageSignedId(form, input, file.xhr.signedId)
})
})
}
const insertImageSignedId = (form, input, signed_id) => {
const hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("value", signed_id)
hiddenField.name = input.name
form.appendChild(hiddenField)
}
if (uppyTriggerElement) {
startUppy()
}
```
And add uppy.js into layout
```erb
<!DOCTYPE html>
<html>
<head>
<title>Uppy Example</title>
<%= stylesheet_link_tag 'admin/default', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
<%= csrf_meta_tags %>
</head>
<body>
<div class="container">
<%= yield %>
<%= javascript_pack_tag 'uppy' %>
</div>
</body>
</html>
```
form example
```erb
<div class="form-group">
<%= f.label "Images" %>
<div id="js-uppy-upload" class="form-control">
Upload
</div>
<%= %>
<%= f.file_field :images, multiple: true, class: "hidden" %>
</div>
```
## 2. Figure out how direct upload works with ActiveStorage
ActuveStorage has it's own direct upload JS client, however, it's not useful in this case because it's only able to upload real file data from local but not able to handle google drive upload, therefore we need to implement our own `direct upload` API for it, thankfully it's easy
Add a new endpoint into your rails project, this simplest implementation is below:
```rb
class UploaderController < ApplicationController
# TODO: Add CSRF protection
skip_before_action :verify_authenticity_token
# TODO: Add authentication, cookie based authentication should be good enough
def image
# XHRUpload will send image 1 by 1
# https://uppy.io/docs/xhr-upload/#bundle-false
file = params[:files].first
blob = ActiveStorage::Blob.create_after_upload!(
io: file,
filename: file.original_filename,
content_type: file.content_type
)
render json: { uploadURL: url_for(blob), signedId: blob.signed_id }
end
end
```
Cons here is that we still need to process image file in our server, I can't find a way to avoid it with ActiveStorage, if you know a better way I'm happy to hear, thanks :)
Now the upload workflow will be
User select files from google drive -> Client tell companion server to upload file to our API -> API server process image, save it to AWS S3 and return ActiveStorage's record's (blob) signed_id -> Client add signed_id into form field and submit with it -> Rails server bind uploaded image with form record
## 3. A companion server (a proxy server to handle google auth)
I use the heroku node template, so there are heroku cli dependency (to load .env file), you can remove this dependency simply by loading `.env` yourself
```json
// package.json
{
"name": "example-uppy-companion-server",
"version": "1.0.0",
"description": "Example uppy companion server",
"engines": {
"node": "10.x"
},
"main": "index.js",
"scripts": {
"start": "heroku local"
},
"dependencies": {
"@uppy/companion": "^0.15.0",
"body-parser": "^1.18.3",
"express": "^4.16.4",
"express-session": "^1.15.6"
},
"devDependencies": {
"request": "^2.81.0",
"tape": "^4.7.0"
}
}
```
```js
// index.js
const express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')
const app = express()
app.use(bodyParser.json())
app.use(session({
secret: 'some-secret',
resave: true,
saveUninitialized: true
}))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
res.setHeader('Access-Control-Allow-Credentials', 'true')
next()
})
// Routes
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.send('Welcome to Companion')
})
// initialize uppy
const uppyOptions = {
providerOptions: {
google: {
key: process.env.PROVIDER_GOOGLE_DRIVE_KEY || "",
secret: process.env.PROVIDER_GOOGLE_DRIVE_SECRET || 5
}
},
server: {
host: process.env.PROVIDER_SERVER_HOST || "localhost:3020",
protocol: process.env.PROVIDER_SERVER_PROTOCOL || "http"
},
filePath: "./output",
secret: "some-secret",
debug: true
};
app.use(companion.app(uppyOptions))
// handle 404
app.use((req, res, next) => {
return res.status(404).json({ message: 'Not Found' })
})
// handle server errors
app.use((err, req, res, next) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
})
companion.socket(app.listen(process.env.PORT || 3020), uppyOptions)
console.log('Welcome to Companion!')
```
```
# .env
PROVIDER_SERVER_HOST=localhost:3020
PROVIDER_SERVER_PROTOCOL=http
PROVIDER_GOOGLE_DRIVE_KEY=PROVIDER_GOOGLE_DRIVE_KEY
PROVIDER_GOOGLE_DRIVE_SECRET=PROVIDER_GOOGLE_DRIVE_SECRET
PORT=3020
```
https://stackoverflow.com/questions/50872912/integration-rails-active-storage-with-js-uploaders
https://github.com/rails/rails/issues/32251
https://medium.com/weareevermore/manual-uploads-using-activestorage-47808dab1b65
2018年11月14日 星期三
Using marked.js with hightlight.js in rails to provide github flavored markdown to github style html
In this example, I use webpacker to import highlight js and marked js and css file, you can use cdn as well.
`app/javascripts/application.js`
```js
import marked from 'marked';
import hljs from 'highlight.js';
marked.setOptions({
highlight: function(code) {
return hljs.highlightAuto(code).value;
},
})
window.marked = marked
```
Let's say we have Post model with `content` field with markdown text
`app/views/posts/show.html.erb`
```erb
<h1><%= @post.title %></h1>
<%= @post.user.email %> - <%= @post.created_at %>
<div id="postContent">
<%= @post.content %>
</div>
<%= content_for :javascripts do %>
<script>
var contentElement = document.querySelector('#postContent')
contentElement.innerHTML = marked(contentElement.innerHTML)
</script>
<% end %>
```
And add application.js into layout by adding this line into `app/views/layout/application.html.erb`
```erb
<%= javascript_pack_tag 'application' %>
```
Now marked.js and hightligh.js are both working properly, check the HTML DOM on post page and you'll see html elements with some auto generated css classes.
However, it's not showing the right styles because we only apply JS but not CSS, now what we need to do is add github style which is provided by highlight.js.
In my other post I've shown how to add it: https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html
In `app/javascripts/styles.js`
```js
import 'highlight.js/styles/github.css';
```
And then add to layout
```erb
<%= stylesheet_pack_tag 'styles' %>
```
And that's it, now the Post show page will show markdown text in HTML format with github styles.
Cheers.
https://github.com/highlightjs/highlight.js/issues/578
https://marked.js.org/#/USING_ADVANCED.md#highlight
https://everyday1percent.blogspot.com/2018/11/import-css-from-npm-module-in-rails.html
標籤:
CSS,
JavaScript,
ROR
Import css from npm module in rails with webpacker
I use highlight.js
In `app/javascripts/styles.js`
```js
import 'highlight.js/styles/github.css';
```
And then add to layout
```erb
<%= stylesheet_pack_tag 'styles' %>
```
Done.
You can simply replace highlight.js with any other module like bootstrap though
https://github.com/rails/webpacker/blob/master/docs/css.md
標籤:
CSS,
JavaScript,
ROR
訂閱:
文章 (Atom)