Is it possible to use startUndoable with custom action in react-admin? - javascript

I wondered if passing a custom action with a custom fetch and type (which is not update) to startUndoable is feasible.
Or is it possible that somehow define a pattern with values in meta and based on this pattern the view would be re-rendered?
In this case the IMPORT is updating only one property in the database with a fixed value.
This is the action:
export const importParcel = ({ id }) => ({
type: IMPORT_PARCEL,
payload: {
id
},
meta: {
resource: 'parcels',
fetch: IMPORT,
refresh: true,
onSuccess: {
notification: {
body: 'Parcel Imported',
level: 'info'
}
},
onFailure: {
notification: {
body: 'Error: Import failed',
level: 'warning'
}
}
}
});
This is the handler:
fetchUtils
.fetchJson(`/${resource}/import/${params.id}`, {
method: 'PUT',
headers: getAuthenticationHeaders()
})
.then(res => ({ data: res.json }));
Thanks for your help! :)

Sure, as explained in the Optimistic Rendering and Undo documentation you can create whatever action you want with startUndoable:
import { startUndoable as startUndoableAction } from 'ra-core';
class App extends Component {
handleImport = () => {
this.props.startUndoable(importParcel());
};
render() {
return <Button onClick={this.handleImport}>Import Parcel</Button>;
}
}
export default connect(null, { startUndoable: startUndoableAction })(App);
You action must have a onSuccess notification in order to display the undo button.
The rest should be implemented in your data provider.

Related

Vue trigger request to fetch data from API

I'm fresh in Vue so this question can be dumb. I want to display data in Vue from my backend Rails API. The data should shows up each time a user enters the site. To do so I'm calling GET endpoint which is located below:
imports.js
const fetchSyncedProductsResultRequest = (self) => {
const jwtToken = self.$store.state.idToken;
return axios
.get(`/api/v1/imports/products/sync_result`, {
headers: {
Authorization: `Bearer ${jwtToken}`,
}
})
.then(response => {
self.unsyncedProducts = response.data['products']
})
};
export {
fetchSyncedProductsResultRequest
};
Expected JSON response from this GET will be:
{:products=>
[{:id=>"611ea9a7392ab50013cf4713", :name=>"2-Tone Hoodie", :code=>"SS22CH013", :result=>nil, :last_sync_at=>nil},
{:id=>"60ec84062f25d400150b351c", :name=>"5-Pocket Denim", :code=>"SS22WP014", :result=>nil, :last_sync_at=>nil},
{:id=>"61966dc83e81dd001731ccd7", :name=>"Zip Shirt Jacket", :code=>"FW22WT001", :result=>nil, :last_sync_at=>nil},
{:id=>"61d5cab6b41408001b0e9376", :name=>"Yankees Satin Varsity Jacket", :code=>"FW22WJ021", :result=>nil, :last_sync_at=>nil}]}
Inside my Vue file I've got:
sync_products.vue
<template>
<div>
<div class="panel">
<div class="panel-body">
<h4>Synchronize products</h4>
<div v-for="product in fetchedProductSyncStatus" :key="product" class="status">
{{product}}
</div>
</div>
</div>
</div>
</template>
<script>
import {
fetchUnsyncedProductsRequest,
} from '../../api/imports'
export default {
name: 'BackboneSyncProducts',
data(){
return{
fetchedProductSyncStatus: []
}
},
}
</script>
<style scoped>
</style>
Looks like request is not sent because nothing shows up and I don't see it in Network tab. What determines the sending of this request?
You need to hook the fetchUnsyncedProductsRequest function to Vue's lifecycles, like created or mounted. See: https://vuejs.org/guide/essentials/lifecycle.html
I would also change the function to just return the data.
const fetchSyncedProductsResultRequest = (token) => {
return axios
.get(`/api/v1/imports/products/sync_result`, {
headers: {
Authorization: `Bearer ${token}`,
}
})
.then(response => {
return response.data['products']
})
};
export {
fetchSyncedProductsResultRequest
};
Then add the created hook and add the response to fetchedProductSyncStatus
export default {
name: 'BackboneSyncProducts',
data() {
return{
fetchedProductSyncStatus: []
}
},
created () {
const jwtToken = this.$store.state.idToken;
fetchUnsyncedProductsRequest(jwtToken).then(data => {
this.fetchedProductSyncStatus = data
})
}
}
Edit: Fixed the self reference error you commented about. On that note it is bad practice to store token in the client like a store

Can't connect custom action with the custom reducer react-admin

8.4 of react-admin. I've been trying to implement a custom action that connects with the custom reducer but so far nothing has worked.
I've Implemented this part of the guide in the official documentation for the action side https://marmelab.com/react-admin/doc/3.8/Actions.html#querying-the-api-with-fetch and this for the reducer https://marmelab.com/react-admin/doc/3.8/Admin.html#customreducers. The problem stems from that I can only use useUpdate method which sends update request, instead of a get without connecting to the reducer and there is no clear explanation of how I can chain those two things together. I also tried using an older way of dispatching actions, but still didn't work. Please help I've been trying this for 2 weeks now. Nothing gets updates and the redux store stays the same.
component
const { data, loading, error } = useQueryWithStore({
type: 'getList',
resource: 'goals',
action: "GET_USER_GOALS",
payload: { pagination: { page: 1, perPage: 10 }, sort: { field: "a-z", order: "ABC" }, filter: {} }
});
reducer
export default (previousState = 0, { type, payload }) => {
console.log(type)
if (type === 'GET_USER_GOALS') {
return payload.rate;
}
return previousState;
}
I even wrote a custom action
but it says that "Cannot read property 'update' of undefined" which isn't supported in the newer version I guess.
import { UPDATE } from 'react-admin';
export const UPDATE_PAGE = 'GET_USER_GOALS';
export const setGoals = (id, data) => {
return {
type: UPDATE_PAGE,
payload: { id, data: { ...data, is_updated: true } },
meta: { fetch: UPDATE, resource: 'goals' },
}
};
admin
<Admin
locale="en"
customReducers={{ userGoals: userGaolsReducer }}
loginPage={LoginPage}
authProvider={authProvider}
dataProvider={testProvider}
i18nProvider={i18nProvider}
history={history}
dashboard={Dashboard}
customSagas={[userGoalsSaga]}
>
I had to include it in the store.js as well
const reducer = combineReducers({
admin: adminReducer,
router: connectRouter(history),
userDashboardSettings: userGaolsReducer
});

How to create a middleware for check role in Nuxtjs

I'm trying to create a middleware for check role of my users.
// middleware/is-admin.js
export default function (context) {
let user = context.store.getters['auth/user']
if ( user.role !== 'admin' ) {
return context.redirect('/errors/403')
}
}
In my .vue file, I'm putting this on:
middleware: [ 'is-admin' ]
It works.
Now, I'd like to check if the user also has another role. So, I create a new middleware:
// middleware/is-consultant.js
export default function (context) {
let user = context.store.getters['auth/user']
if ( user.role !== 'consultant' ) {
return context.redirect('/errors/403')
}
}
And in my .vue file:
middleware: [ 'is-admin', 'is-consultant' ]
Unfortunately, when I do that, if I visit the route with an administrator role, it does not work anymore.
Can you tell me how I can create a middleware that checks multiple roles with Nuxt.js?
Thank you!
The idea is that every page has its authority level. Then in middleware you can compare your current user authority level with the current page authority level, and if it's lower redirect the user. It's very elegant solution that was proposed by Nuxt.js creator. GitHub issue.
<template>
<h1>Only an admin can see this page</h1>
</template>
<script>
export default {
middleware: 'auth',
meta: {
auth: { authority: 2 }
}
}
</script>
Then in your middleware/auth.js:
export default ({ store, route, redirect }) => {
// Check if user is connected first
if (!store.getters['user/user'].isAuthenticated) return redirect('/login')
// Get authorizations for matched routes (with children routes too)
const authorizationLevels = route.meta.map((meta) => {
if (meta.auth && typeof meta.auth.authority !== 'undefined')
return meta.auth.authority
return 0
})
// Get highest authorization level
const highestAuthority = Math.max.apply(null, authorizationLevels)
if (store.getters['user/user'].details.general.authority < highestAuthority) {
return error({
statusCode: 401,
message: 'Du måste vara admin för att besöka denna sidan.'
})
}
}
You can use this feature in Nuxt
export default function ({ $auth, redirect }) {
if (!$auth.hasScope('admin')) {
return redirect('/')
}
}
The scope can be anything you want e.g Consultant, Editor etc.
Check the documentation
Updated
Since you are using Laravel
You can have a role column in your user table
e.g
$table->enum('role', ['subscriber', 'admin', 'editor', 'consultant', 'writer'])->default('subscriber');
Then create a API resource, check the documentation for more
To create a user resource, run this artisan
php artisan make:resource UserResource
Then in your resource, you can have something like this
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'gender' => $this->gender,
'country' => $this->country,
'avatar' => $this->avatar,
'role' => $this->role,
];
}
Then you can import it to your controller like this
use App\Http\Resources\UserResource;
You can get the resource like this
$userdata = new UserResource(User::find(auth()->user()->id));
return response()->json(array(
'user' => $userdata,
));
In Nuxt
To do authentication in Nuxt
Install nuxt auth and axios
Using YARN : yarn add #nuxtjs/auth #nuxtjs/axios
Or using NPM: npm install #nuxtjs/auth #nuxtjs/axios
Then register them in your nuxtconfig.js
modules: [
'#nuxtjs/axios',
'#nuxtjs/auth',
],
In your nuxtconfig.js, add this also
axios: {
baseURL: 'http://127.0.0.1:8000/api'
},
auth: {
strategies: {
local: {
endpoints: {
login: { url: '/login', method: 'post', propertyName: 'access_token' },
logout: { url: '/logout', method: 'post' },
user: { url: '/user', method: 'get', propertyName: false }
},
tokenRequired: true,
tokenType: 'Bearer',
globalToken: true
// autoFetchUser: true
}
}
}
The URL been the endpoints
Check Documentation for more
To restrict certain pages in Nuxt to Specific User.
> Create a middlweare e.g isadmin.js
Then add this
export default function ({ $auth, redirect }) {
if (!$auth.hasScope('admin')) {
return redirect('/')
}
}
Then go to the Page, add the middleware
export default {
middleware: 'isadmin'

Accessing nuxt $store inside Dynamic Component

I'm developing a Promise-based modal component which provides the possibility of specifing a component as body of the modal itself. To achieve that result, I thought that a good solution would be using a dynamic component inside the modal template.
However, inside a NUXT application, if the component refers to the Vuex instance (this.$store), it turns out to be undefined (or better there is no $store object attribute). In the same way, any injection done inside plugins results undefined (e.g. inject('api', api) create the attribute $api, but it results undefined).
If I just use the component in the 'standard' way (e.g. placing it inside the page or another component template), everything works fine.
There should be some 'extra injection' that I should do before passing the component in a programmatic way.
Can anyone help me?
The NUXT project structure (simplified):
/pages/index.vue
/plugins/api.js
/store/auth.js
/components/HelloComponent.vue
/plugins/api.js
let api = {}
api.call = function (request, auth, unpack, axios = this.axios) {
if (!request) Error('backend.js:call invalid params:', request, auth, unpack, axios)
if (auth) {
if (request.headers)
request.headers['Authorization'] = 'Bearer ' + this.auth.accessToken
else
request.headers = { 'Authorization': 'Bearer ' + this.auth.accessToken }
}
return axios(request).then((response) => unpack ? response.data : response)
}
api.getAPI = function (api, params, auth = true, unpack = true) {
if (!api) Error('api.js:getAPI invalid params:', api)
console.log('api.js:getAPI api:', api)
return this.call({ method: 'get', url: api, params: params }, auth, unpack)
}
api.postAPI = function (api, params, data, auth = true, unpack = true) {
if (!api) Error('api.js:postAPI invalid params:', api, data)
console.log('api.js:postAPI api:', api)
return this.call({ method: 'post', url: api, params: params, data: data }, auth, unpack)
}
/*******************************************************/
/* NUXT plugin and reference injection */
/*******************************************************/
export default function (context, inject) {
console.log('[CALL] api.js')
/* assign global $axios instance */
api.axios = context.$axios
/* assign auth instance to access tokens */
api.auth = context.store.state.auth
/* inject backend reference into application instance */
inject('api', api)
}
/pages/index.vue
<template>
<div>
<span>
{{ $store.auth.state.name }} // -> Displays 'Chuck'
</span>
/* Object.keys(this).includes('$store): false'; Object.keys(this).includes('$auth): true' */
<component :is="cComponent" /> // -> this.$store is undefined; auth: undefined
<hello-component /> // -> Displays 'Chuck'; auth: Object {...}
</div>
</template>
<script>
import HelloComponent from '../components/HelloComponent.vue'
export default {
components: {
HelloComponent
},
created () {
this.$store.commit('auth/setName', 'Chuck')
},
computed: {
cComponent () {
return HelloComponent
}
}
}
</script>
/components/HelloComponent.vue
<template>
<span>
{{ $store.auth.state.name }}
</span>
</template>
<script>
export default {
created() {
console.log('auth:', this.$auth)
}
}
</script>
/store/auth.js
export const state = () => ({
accessToken: null,
refreshToken: null,
name: null,
})
export const mutations = {
setAccessToken(state, token) {
console.info('auth.js:setAccessToken', token)
state.accessToken = token
},
setRefreshToken(state, token) {
console.info('auth.js:setRefreshToken', token)
state.refreshToken = token
},
setName(state, name) {
console.info('auth.js:setName', name)
state.user = name
},
}
if you have no access of this pointer in Nuxt project, And you really need to access store, then simply use
window.$nuxt.$store instead of this.$store;
Hope it will solve your problem

VueJS multiple components in one view

I'm trying to learn VueJS and it's going well but i run into one problem where i can't get multiple components to work on one page, for some reason the html will load but everything in my export default wont work.
So i have 2 components: a ShortenerComponent and a StatsComponent the ShortenerComponent works and does everything it should, but the StatsComponent will only load the html and will not do anything inside the export default this is my code:
StatsComponent.vue (the ShortenerComponent is the same except for the methods and html.):
<script>
// get the csrf token from the meta
var csrf_token = $('meta[name="csrf-token"]').attr('content');
export default {
data() {
return {
};
},
test() {
this.getStats();
},
methods: {
// get all the existing urls
getStats() {
console.log('console log something');
axios.get('urls').then((response) => {
console.log('console log something');
});
},
}
}
My shortenercomponent:
<script>
// get the csrf token from the meta
var csrf_token = $('meta[name="csrf-token"]').attr('content');
export default {
data() {
return {
shortUrl: '',
url: '',
error: '',
};
},
methods: {
createUrl() {
axios({
method: 'post',
url: '/',
data: {
_token: csrf_token,
url: this.url
},
}).then(response => {
console.log(response);
this.shortUrl = response.data.hash;
}).catch(error => {
this.error = error.response.data.message;
});
}
}
}
and last but not least my app.js file
Vue.component('shortener',require('./components/ShortenerComponent.vue'));
Vue.component('stats', require('./components/StatsComponent.vue'));
var vue = new Vue({
el: '#app',
});
I hope someone could figure out what i did wrong :D
So in my code i had the test method, i thought that would call the getStats method. What i did not now is that Vue has the created method used to run code after an instance is created.
So what i had to do was:
function created()
{
this.getStats();
}
Source: Vue instance Lifecycle Hooks

Categories