Adding Vue.js apps for reviews service
This commit is contained in:
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
@@ -4,6 +4,15 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"name": "bookshop",
|
||||
"command": "cds watch bookshop",
|
||||
|
||||
@@ -9,7 +9,7 @@ const books = new Vue ({
|
||||
|
||||
data: {
|
||||
list: [],
|
||||
book: { descr:'( click on a row to see details... )' },
|
||||
book: undefined,
|
||||
order: { amount:1, succeeded:'', failed:'' }
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ const books = new Vue ({
|
||||
},
|
||||
|
||||
async submitOrder () {
|
||||
const {book,order} = books, amount = parseInt (order.amount) || 1
|
||||
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
||||
try {
|
||||
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||
book.stock = res.data.stock
|
||||
|
||||
@@ -7,22 +7,21 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
||||
<style>
|
||||
#books tr:hover { background: #f2f2f2; cursor: pointer; }
|
||||
form { float:right; display:flex; flex-direction: row-reverse }
|
||||
form #amount { width: 5em }
|
||||
.is-success { color: #0d920d }
|
||||
.has-error { color: #df1010 }
|
||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
||||
.rating-stars { color:teal }
|
||||
.succeeded { color:teal }
|
||||
.failed { color:red }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="small-container", style="margin-top: 70px;">
|
||||
<div id='app'>
|
||||
|
||||
<h1> Capire Books </h1>
|
||||
<h1> {{ document.title }} </h1>
|
||||
|
||||
<input type="text" placeholder="Search..." @input="search">
|
||||
|
||||
<table id='books'>
|
||||
<table id='books' class="hovering">
|
||||
<thead>
|
||||
<th> Book </th>
|
||||
<th> Author </th>
|
||||
@@ -34,29 +33,30 @@
|
||||
<td>{{ book.title }}</td>
|
||||
<td>{{ book.author }}</td>
|
||||
<td>{{ book.genre.name }}</td>
|
||||
<td style="color:teal">{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}</td>
|
||||
<td class="rating-stars">
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||
</td>
|
||||
<td>{{ book.currency.symbol }} {{ book.price }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div v-if="book.title">
|
||||
<div v-if="book">
|
||||
<img v-bind:src="book.image" alt=""/>
|
||||
</div>
|
||||
|
||||
<div v-if="book.title">
|
||||
<label style="text-align:right">
|
||||
<span class="is-success"> {{ order.succeeded }} </span>
|
||||
<span class="has-error"> {{ order.failed }} </span>
|
||||
<span class="succeeded"> {{ order.succeeded }} </span>
|
||||
<span class="failed"> {{ order.failed }} </span>
|
||||
{{ book.stock }} in stock
|
||||
</label>
|
||||
<form @submit.prevent="submitOrder">
|
||||
<input type="number" id="amount" v-model="order.amount" v-bind:class="{ 'has-error': order.failed }">
|
||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||
<input type="submit" value="Order:" class="muted-button">
|
||||
</form>
|
||||
<h4> {{ book.title }} </h4>
|
||||
<p> {{ book.descr }} </p>
|
||||
</div>
|
||||
<div v-else>
|
||||
( click on a row to see details... )
|
||||
</div>
|
||||
|
||||
<h4> {{ book.title }} </h4>
|
||||
<p> {{ book.descr }} </p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
2
fiori/.env
Normal file
2
fiori/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
# cds.requires.messaging.kind = file-based-messaging
|
||||
PORT = 4004
|
||||
@@ -1,54 +1,48 @@
|
||||
|
||||
@me = {{$processEnv USER}}:
|
||||
@bookshop = http://localhost:4004
|
||||
@reviews-service = {{bookshop}}/reviews
|
||||
|
||||
# Uncomment this when running separate reviews service
|
||||
# @reviews-service = http://localhost:5005/reviews
|
||||
# Uncomment this when running a separate reviews service
|
||||
@reviews-service = http://localhost:4005/reviews
|
||||
|
||||
|
||||
|
||||
#################################################
|
||||
#
|
||||
# To ReviewsService
|
||||
# Reviews Service
|
||||
#
|
||||
# move the right down:
|
||||
|
||||
### Get all reviews
|
||||
GET {{reviews-service}}/Reviews
|
||||
|
||||
### Add a new review (with random rating)
|
||||
POST {{reviews-service}}/Reviews
|
||||
Content-Type: application/json;IEEE754Compatible=true
|
||||
Authorization: Basic {{me}}
|
||||
###
|
||||
|
||||
{"subject":"201", "title":"boo" }
|
||||
POST {{reviews-service}}/Reviews
|
||||
Authorization: Basic {{$processEnv USER}}:
|
||||
Content-Type: application/json
|
||||
|
||||
{"subject":"201", "title":"boo", "rating":3 }
|
||||
|
||||
|
||||
|
||||
#################################################
|
||||
#
|
||||
# Bookshop Requests involving reviews
|
||||
# (both in-process as well as separate one)
|
||||
# Bookshop Services
|
||||
#
|
||||
|
||||
### Request to CatalogService > delegated to ReviewsService
|
||||
GET {{bookshop}}/browse/Books(201)/reviews?
|
||||
&$select=rating,date,reviewer,title
|
||||
|
||||
### Alternative OData URL
|
||||
GET {{bookshop}}/browse/Books/201/reviews?
|
||||
&$select=rating,date,title
|
||||
&$top=3
|
||||
|
||||
###
|
||||
|
||||
GET {{bookshop}}/browse/Books(201)?
|
||||
&$select=ID,title,rating
|
||||
&$expand=reviews
|
||||
# Note: the $expand only works in case of ReviewsService in same process
|
||||
|
||||
|
||||
|
||||
###
|
||||
#################################################
|
||||
#
|
||||
# Bookshop Services
|
||||
#
|
||||
|
||||
GET {{bookshop}}/orders/Orders
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
cds.requires.messaging.kind = file-based-messaging
|
||||
PORT = 4005
|
||||
PORT = 4006
|
||||
@@ -1,2 +1,2 @@
|
||||
cds.requires.messaging.kind = file-based-messaging
|
||||
PORT = 5005
|
||||
PORT = 4005
|
||||
72
reviews/app/vue/app.js
Normal file
72
reviews/app/vue/app.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* global Vue axios */ //> from vue.html
|
||||
const $ = sel => document.querySelector(sel)
|
||||
const GET = (url) => axios.get('/reviews'+url)
|
||||
const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data)
|
||||
const POST = (cmd,data) => axios.post('/reviews'+cmd,data)
|
||||
|
||||
const reviews = new Vue ({
|
||||
|
||||
el:'#app',
|
||||
|
||||
data: {
|
||||
list: [],
|
||||
review: undefined,
|
||||
message: {},
|
||||
Ratings: Object.entries({
|
||||
5 : '★★★★★',
|
||||
4 : '★★★★',
|
||||
3 : '★★★',
|
||||
2 : '★★',
|
||||
1 : '★',
|
||||
}).reverse()
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
search: ({target:{value:v}}) => reviews.fetch(v && '&$search='+v),
|
||||
|
||||
async fetch (etc='') {
|
||||
const {data} = await GET(`/Reviews?${etc}`)
|
||||
reviews.list = data.value
|
||||
},
|
||||
|
||||
async inspect (eve) {
|
||||
const review = reviews.review = reviews.list [eve.currentTarget.rowIndex-1]
|
||||
const res = await GET(`/Reviews/${review.ID}/text/$value`)
|
||||
review.text = res.data
|
||||
reviews.message = {}
|
||||
},
|
||||
|
||||
async newReview () {
|
||||
reviews.review = {}
|
||||
reviews.message = {}
|
||||
setTimeout (()=> $('form > input').focus(), 111)
|
||||
},
|
||||
|
||||
async submitReview () {
|
||||
const review = reviews.review; review.rating = parseInt (review.rating) // REVISIT: Okra should be less strict
|
||||
try {
|
||||
if (!review.ID) {
|
||||
const res = await POST(`/Reviews`,review)
|
||||
reviews.ID = res.data.ID
|
||||
} else {
|
||||
console.trace()
|
||||
await PUT(`/Reviews/${review.ID}`,review)
|
||||
}
|
||||
reviews.message = { succeeded: 'Your review was submitted successfully. Thanks.' }
|
||||
} catch (e) {
|
||||
reviews.message = { failed: e.response.data.error.message }
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
filters: {
|
||||
stars: (r) => ('★'.repeat(Math.round(r))+'☆☆☆☆☆').slice(0,5),
|
||||
datetime: (d) => d && new Date(d).toLocaleString(),
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
// initially fill list of my reviews
|
||||
reviews.fetch()
|
||||
62
reviews/app/vue/index.html
Normal file
62
reviews/app/vue/index.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title> Capire Reviews </title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
||||
<style>
|
||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
||||
.rating-stars { color:teal }
|
||||
.succeeded { color:teal }
|
||||
.failed { color:red }
|
||||
textarea { line-height: 1.4em;}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="small-container", style="margin-top: 70px;">
|
||||
<div id='app'>
|
||||
|
||||
<h1> {{ document.title }} </h1>
|
||||
|
||||
<input type="text" placeholder="Search..." @input="search">
|
||||
|
||||
<table id='my-reviews' class="hovering">
|
||||
<thead>
|
||||
<th> Subject </th>
|
||||
<th> Rating </th>
|
||||
<th> Title </th>
|
||||
<th> Date </th>
|
||||
</thead>
|
||||
<tr v-for="review in list" v-bind:id="review.ID" v-on:click="inspect">
|
||||
<td>{{ review.subject }}</td>
|
||||
<td class="rating-stars">{{ review.rating | stars }}</td>
|
||||
<td>{{ review.title }}</td>
|
||||
<td>{{ review.date | datetime }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<button v-on:click="newReview()" class="round-button muted-button">Add Review...</button>
|
||||
|
||||
<form v-if="review" @submit.prevent="submitReview">
|
||||
<input id="subject" type="text" v-model="review.subject" style="font-weight:bold; display:inline; width:20%">
|
||||
<input type="text" v-model="review.title" style="font-weight:bold; display:inline; width:60%">
|
||||
<select v-model="review.rating" style="font-weight:bold; display:inline; width:17%; float: right;">
|
||||
<option v-for="option in Ratings" v-bind:value="option[0]"> {{ option[1] }} </option>
|
||||
</select>
|
||||
<textarea v-model="review.text" rows="9"></textarea>
|
||||
<input type="submit" value="Submit" class="round-button muted-button">
|
||||
<span class="succeeded"> {{ message.succeeded }} </span>
|
||||
<span class="failed"> {{ message.failed }} </span>
|
||||
</form>
|
||||
<div v-else style="margin-top: 2em;">
|
||||
( click on a row to see details... )
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</html>
|
||||
5
reviews/db/data/sap.capire.reviews-Reviews.csv
Normal file
5
reviews/db/data/sap.capire.reviews-Reviews.csv
Normal file
@@ -0,0 +1,5 @@
|
||||
subject;rating;title;text
|
||||
201;5;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
201;4;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
|
||||
207;2;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
|
||||
251;3;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
|
||||
|
@@ -1 +1,2 @@
|
||||
using from './srv/reviews-service';
|
||||
namespace sap.capire.reviews;
|
||||
|
||||
@@ -27,7 +27,14 @@ service ReviewsService {
|
||||
annotate ReviewsService.Reviews with @restrict:[
|
||||
{ grant:'READ', to:'any' }, // everybody can read reviews
|
||||
{ grant:'CREATE', to:'authenticated-user' }, // users must login to add reviews
|
||||
{ grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' },
|
||||
/////////////////////////////////////////////////
|
||||
//
|
||||
// Temporarily disabling this due to glitch in CAP Node.js runtime:
|
||||
// { grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' },
|
||||
// -> reenable it when the issue is fixed
|
||||
{ grant:'UPDATE', to:'authenticated-user' },
|
||||
//
|
||||
////////////////////////////////////////////////////
|
||||
{ grant:'DELETE', to:'admin' },
|
||||
];
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('Messaging', ()=>{
|
||||
|
||||
it ('should bootstrap sqlite in-memory db', async()=>{
|
||||
const db = await cds.deploy (_model) .to ('sqlite::memory:')
|
||||
await db.delete('Reviews')
|
||||
expect (db.model) .not.undefined
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user