chore: add initial example
This commit is contained in:
Родитель
eb5be5cf53
Коммит
648cc4569b
|
@ -348,3 +348,7 @@ MigrationBackup/
|
|||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# React App node_modules and package-lock.json
|
||||
**/**/package-lock.json
|
||||
**/**/node_modules
|
|
@ -1,2 +1,6 @@
|
|||
# kendo-react-datagrid-asp-net-core
|
||||
# Kendo React Data Grid ASP.NET Core
|
||||
This repository contains a sample application showcasing how to implement common data operations for the KendoReact Data Grid with an ASP.NET Core backend. The project focuses on how data operations like paging, sorting, filtering, and grouping are triggered on the client and can be handled on the sever-side of things. The ultimate goal is to showcase a full-stack project and demystify the client and server-side interactions with an advanced UI component like the data grid.
|
||||
|
||||
This example is made with .NET version 6.
|
||||
|
||||
We have another example made with a older version of .NET and ASP.NET Core which is also showing a Grid with editing. You can find it here: https://github.com/telerik/kendo-react-demo-aspnetcore-data
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.31903.286
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kendo-react-grid-crud", "kendo-react-grid-crud\kendo-react-grid-crud.csproj", "{FFC169F6-AC82-4854-9CC2-0CCEB1CD6D68}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FFC169F6-AC82-4854-9CC2-0CCEB1CD6D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FFC169F6-AC82-4854-9CC2-0CCEB1CD6D68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FFC169F6-AC82-4854-9CC2-0CCEB1CD6D68}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FFC169F6-AC82-4854-9CC2-0CCEB1CD6D68}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {926949BE-7D53-4FC2-AFE3-6EC17FC532C9}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,232 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
bin/
|
||||
Bin/
|
||||
obj/
|
||||
Obj/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
/wwwroot/dist/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Microsoft Azure ApplicationInsights config file
|
||||
ApplicationInsights.config
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
/node_modules
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
|
@ -0,0 +1 @@
|
|||
BROWSER=none
|
|
@ -0,0 +1,3 @@
|
|||
PORT=44487
|
||||
HTTPS=true
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,33 @@
|
|||
// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate
|
||||
const fs = require('fs');
|
||||
const spawn = require('child_process').spawn;
|
||||
const path = require('path');
|
||||
|
||||
const baseFolder =
|
||||
process.env.APPDATA !== undefined && process.env.APPDATA !== ''
|
||||
? `${process.env.APPDATA}/ASP.NET/https`
|
||||
: `${process.env.HOME}/.aspnet/https`;
|
||||
|
||||
const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
|
||||
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;
|
||||
|
||||
if (!certificateName) {
|
||||
console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
spawn('dotnet', [
|
||||
'dev-certs',
|
||||
'https',
|
||||
'--export-path',
|
||||
certFilePath,
|
||||
'--format',
|
||||
'Pem',
|
||||
'--no-password',
|
||||
], { stdio: 'inherit', })
|
||||
.on('exit', (code) => process.exit(code));
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// This script configures the .env.development.local file with additional environment variables to configure HTTPS using the ASP.NET Core
|
||||
// development certificate in the webpack development proxy.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const baseFolder =
|
||||
process.env.APPDATA !== undefined && process.env.APPDATA !== ''
|
||||
? `${process.env.APPDATA}/ASP.NET/https`
|
||||
: `${process.env.HOME}/.aspnet/https`;
|
||||
|
||||
const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
|
||||
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;
|
||||
|
||||
if (!certificateName) {
|
||||
console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||
|
||||
if (!fs.existsSync('.env.development.local')) {
|
||||
fs.writeFileSync(
|
||||
'.env.development.local',
|
||||
`SSL_CRT_FILE=${certFilePath}
|
||||
SSL_KEY_FILE=${keyFilePath}`
|
||||
);
|
||||
} else {
|
||||
let lines = fs.readFileSync('.env.development.local')
|
||||
.toString()
|
||||
.split('\n');
|
||||
|
||||
let hasCert, hasCertKey = false;
|
||||
for (const line of lines) {
|
||||
if (/SSL_CRT_FILE=.*/i.test(line)) {
|
||||
hasCert = true;
|
||||
}
|
||||
if (/SSL_KEY_FILE=.*/i.test(line)) {
|
||||
hasCertKey = true;
|
||||
}
|
||||
}
|
||||
if (!hasCert) {
|
||||
fs.appendFileSync(
|
||||
'.env.development.local',
|
||||
`\nSSL_CRT_FILE=${certFilePath}`
|
||||
);
|
||||
}
|
||||
if (!hasCertKey) {
|
||||
fs.appendFileSync(
|
||||
'.env.development.local',
|
||||
`\nSSL_KEY_FILE=${keyFilePath}`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"name": "kendo_react_grid_crud",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@progress/kendo-data-query": "^1.5.5",
|
||||
"@progress/kendo-drawing": "^1.15.0",
|
||||
"@progress/kendo-licensing": "^1.2.1",
|
||||
"@progress/kendo-react-animation": "^4.10.0",
|
||||
"@progress/kendo-react-data-tools": "^4.10.0",
|
||||
"@progress/kendo-react-dateinputs": "^4.10.0",
|
||||
"@progress/kendo-react-dropdowns": "^4.10.0",
|
||||
"@progress/kendo-react-grid": "^4.10.0",
|
||||
"@progress/kendo-react-inputs": "^4.10.0",
|
||||
"@progress/kendo-react-intl": "^4.10.0",
|
||||
"@progress/kendo-react-treelist": "^4.10.0",
|
||||
"@progress/kendo-react-treeview": "^4.10.0",
|
||||
"@progress/kendo-theme-default": "^4.42.0",
|
||||
"axios": "^0.24.0",
|
||||
"bootstrap": "^5.1.0",
|
||||
"http-proxy-middleware": "^0.19.1",
|
||||
"jquery": "^3.5.1",
|
||||
"merge": "^2.1.1",
|
||||
"oidc-client": "^1.11.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-bootstrap": "^0.25.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"reactstrap": "^8.9.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"workbox-background-sync": "^5.1.3",
|
||||
"workbox-broadcast-update": "^5.1.3",
|
||||
"workbox-cacheable-response": "^5.1.3",
|
||||
"workbox-core": "^5.1.3",
|
||||
"workbox-expiration": "^5.1.3",
|
||||
"workbox-google-analytics": "^5.1.3",
|
||||
"workbox-navigation-preload": "^5.1.3",
|
||||
"workbox-precaching": "^5.1.3",
|
||||
"workbox-range-requests": "^5.1.3",
|
||||
"workbox-routing": "^5.1.3",
|
||||
"workbox-strategies": "^5.1.3",
|
||||
"workbox-streams": "^5.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "^6.9.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-plugin-flowtype": "^5.7.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"nan": "^2.14.2",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "node aspnetcore-https && node aspnetcore-react",
|
||||
"start": "rimraf ./build && react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "cross-env CI=true react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint ./src/"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 5.3 KiB |
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<base href="%PUBLIC_URL%/" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>kendo_react_grid_crud</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "kendo_react_grid_crud",
|
||||
"name": "kendo_react_grid_crud",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Route } from 'react-router';
|
||||
import { Layout } from './components/Layout';
|
||||
import { Home } from './components/Home';
|
||||
import { Counter } from './components/Counter';
|
||||
|
||||
import './custom.css'
|
||||
|
||||
export default class App extends Component {
|
||||
static displayName = App.name;
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Layout>
|
||||
<Route exact path='/' component={Home} />
|
||||
<Route path='/counter' component={Counter} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', async () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
<MemoryRouter>
|
||||
<App />
|
||||
</MemoryRouter>, div);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
export class Counter extends Component {
|
||||
static displayName = Counter.name;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { currentCount: 0 };
|
||||
this.incrementCounter = this.incrementCounter.bind(this);
|
||||
}
|
||||
|
||||
incrementCounter() {
|
||||
this.setState({
|
||||
currentCount: this.state.currentCount + 1
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p>This is a simple example of a React component.</p>
|
||||
|
||||
<p aria-live="polite">Current count: <strong>{this.state.currentCount}</strong></p>
|
||||
|
||||
<button className="k-button" onClick={this.incrementCounter}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
import '@progress/kendo-theme-default/dist/all.css'
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { Grid, GridColumn, GridToolbar } from '@progress/kendo-react-grid';
|
||||
import { mapTree } from "@progress/kendo-react-treelist";
|
||||
import { clone } from '@progress/kendo-react-common';
|
||||
|
||||
/**
|
||||
* Import a custom command cell responsible for rendering the Edit, Remove, Update and Cancel commands.
|
||||
*/
|
||||
import MyCommandCell from './cells/CommandCell';
|
||||
|
||||
/**
|
||||
* Import a custom cell responsible for rendering and editing complext fields with a DropDownList.
|
||||
*/
|
||||
import DropDownCell from './cells/DropDownCell';
|
||||
|
||||
/**
|
||||
* Import the React.Context use to pass extra props to the custom cell.
|
||||
*/
|
||||
import DataContext from '../contexts/data-context';
|
||||
|
||||
import Axios from "axios";
|
||||
import { toDataSourceRequestString, translateDataSourceResultGroups } from '@progress/kendo-data-query';
|
||||
|
||||
const Home = (props) => {
|
||||
const [data, setData] = useState([]);
|
||||
const [itemBeforeEdit, setItemBeforeEdit] = useState({})
|
||||
const [dataState, setDataState] = useState({ take: 5, skip: 0, group: [] })
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
// Check if the Grid is grouped.
|
||||
const hasGroups = dataState.group && dataState.group.length;
|
||||
|
||||
|
||||
// Make request to the server for server side data operations.
|
||||
useEffect(() => {
|
||||
Axios.post("https://localhost:44487/products", toDataSourceRequestString(dataState)).then((response) => {
|
||||
let parsedDataNew = mapTree(response.data.data, 'items', (product) => {
|
||||
product.firstOrderedOn = product.firstOrderedOn !== null ? new Date(product.firstOrderedOn) : null;
|
||||
return product
|
||||
})
|
||||
parsedDataNew = hasGroups ? translateDataSourceResultGroups(parsedDataNew) : parsedDataNew
|
||||
setTotal(response.data.total)
|
||||
setData([...parsedDataNew]);
|
||||
});
|
||||
}, [dataState]) // We make the request initially and everytime the dataState is changed.
|
||||
|
||||
|
||||
/**
|
||||
* Add a new empty item only to the local data.
|
||||
*/
|
||||
const addRecord = () => {
|
||||
let newDate = new Date();
|
||||
|
||||
// We use this to remvoe time porting of the date based on the timezone. In this example we edit and filter only the date portion.
|
||||
var myToday = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate(), 0, 0, 0);
|
||||
let offsetMiliseconds = new Date().getTimezoneOffset() * 60000;
|
||||
let dateWitnNotimeZone = new Date(
|
||||
myToday.getTime() - offsetMiliseconds
|
||||
);
|
||||
let newRecord = { productID: null, firstOrderedOn: dateWitnNotimeZone, category: { categoryID: 1, categoryName: "Beverages"}, inEdit: true }
|
||||
let newData = [...data];
|
||||
newData.unshift(newRecord);
|
||||
setData(newData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the local data when the user edits a field.
|
||||
*/
|
||||
const handleItemChange = (event) => {
|
||||
let newData = mapTree(data, 'items', item => {
|
||||
if (event.dataItem.productID === item.productID) {
|
||||
item[event.field] = event.value;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the row in edit mode.
|
||||
*/
|
||||
const enterEdit = (dataItem) => {
|
||||
let newData = mapTree(data, "items", (item) => {
|
||||
dataItem.productID === item.productID ? item.inEdit = true : item.inEdit = false;
|
||||
return item;
|
||||
});
|
||||
|
||||
setItemBeforeEdit(clone(dataItem));
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the server to delete a specific item.
|
||||
*/
|
||||
const remove = (dataItem) => {
|
||||
// We process the dataState to make it compatible with the server requirements.
|
||||
const data = {
|
||||
filter: toDataSourceRequestString({ filter: dataState.filter }).replace("filter=", ""),
|
||||
sort: toDataSourceRequestString({ sort: dataState.sort }).replace("sort=", ""),
|
||||
group: toDataSourceRequestString({ group: dataState.group }).replace("group=", ""),
|
||||
take: dataState.take,
|
||||
skip: dataState.skip
|
||||
}
|
||||
Axios.delete("https://localhost:44487/products", { data: { ...data, product: dataItem }}).then(
|
||||
(response) => {
|
||||
let parsedDataNew = mapTree(response.data.data, 'items', item => {
|
||||
item.firstOrderedOn = new Date(item.firstOrderedOn);
|
||||
return item;
|
||||
})
|
||||
parsedDataNew = hasGroups ? translateDataSourceResultGroups(parsedDataNew) : parsedDataNew
|
||||
setData(parsedDataNew);
|
||||
setTotal(response.data.total);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the server to create a new item.
|
||||
*/
|
||||
const add = (dataItem) => {
|
||||
let headers = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
// We process the dataState to make it compatible with the server requirements.
|
||||
const data = {
|
||||
filter: toDataSourceRequestString({ filter: dataState.filter }).replace("filter=", ""),
|
||||
sort: toDataSourceRequestString({ sort: dataState.sort }).replace("sort=", ""),
|
||||
group: toDataSourceRequestString({ group: dataState.group }).replace("group=", ""),
|
||||
take: dataState.take,
|
||||
skip: dataState.skip
|
||||
}
|
||||
Axios.put("https://localhost:44487/products", { ...data, product: dataItem }, headers).then(
|
||||
(response) => {
|
||||
let parsedDataNew = mapTree(response.data.data, 'items', item => {
|
||||
item.firstOrderedOn = new Date(item.firstOrderedOn);
|
||||
return item;
|
||||
})
|
||||
parsedDataNew = hasGroups ? translateDataSourceResultGroups(parsedDataNew) : parsedDataNew
|
||||
setData(parsedDataNew);
|
||||
setTotal(response.data.total);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a new item that has not been still saved on the server.
|
||||
*/
|
||||
const discard = () => {
|
||||
let hasGroup = dataState.group.length > 0 ? true : false
|
||||
let newData = []
|
||||
hasGroup ? newData = data.filter(item => item.value !== undefined) : newData = data.filter(item => item.productID !== null)
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the server to update a specific item.
|
||||
*/
|
||||
const update = (dataItem) => {
|
||||
// We use this to remvoe time porting of the date based on the timezone. In this example we edit and filter only the date portion.
|
||||
var myToday = new Date(dataItem.firstOrderedOn.getFullYear(), dataItem.firstOrderedOn.getMonth(), dataItem.firstOrderedOn.getDate(), 0, 0, 0);
|
||||
let offsetMiliseconds = new Date().getTimezoneOffset() * 60000;
|
||||
let dateWitnNotimeZone = new Date(
|
||||
myToday.getTime() - offsetMiliseconds
|
||||
);
|
||||
|
||||
dataItem.firstOrderedOn = dateWitnNotimeZone;
|
||||
|
||||
let headers = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
|
||||
// We process the dataState to make it compatible with the server requirements.
|
||||
const data = {
|
||||
filter: toDataSourceRequestString({ filter: dataState.filter }).replace("filter=", ""),
|
||||
sort: toDataSourceRequestString({ sort: dataState.sort }).replace("sort=", ""),
|
||||
group: toDataSourceRequestString({ group: dataState.group }).replace("group=", ""),
|
||||
take: dataState.take,
|
||||
skip: dataState.skip
|
||||
}
|
||||
Axios.put("https://localhost:44487/products", { ...data, product: dataItem }, headers).then(
|
||||
(response) => {
|
||||
let parsedDataNew = mapTree(response.data.data, 'items', item => {
|
||||
item.firstOrderedOn = new Date(item.firstOrderedOn);
|
||||
return item;
|
||||
})
|
||||
parsedDataNew = hasGroups ? translateDataSourceResultGroups(parsedDataNew) : parsedDataNew
|
||||
setData(parsedDataNew);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the changes made to an item before they were saved on the server.
|
||||
*/
|
||||
const cancel = () => {
|
||||
let newData = mapTree(data, 'items', item => {
|
||||
if (item.productID === itemBeforeEdit.productID) {
|
||||
item = itemBeforeEdit;
|
||||
item.inEdit = false;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
const handleDataStateChange = (event) => {
|
||||
setDataState(event.dataState);
|
||||
}
|
||||
return (
|
||||
<DataContext.Provider
|
||||
value={{
|
||||
enterEdit: enterEdit,
|
||||
remove: remove,
|
||||
add: add,
|
||||
discard: discard,
|
||||
update: update,
|
||||
cancel: cancel
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
style={{
|
||||
height: "520px",
|
||||
}}
|
||||
data={data}
|
||||
editField="inEdit"
|
||||
onItemChange={handleItemChange}
|
||||
onDataStateChange={handleDataStateChange}
|
||||
{...dataState}
|
||||
pageable
|
||||
sortable
|
||||
filterable
|
||||
groupable
|
||||
total={total}
|
||||
>
|
||||
<GridToolbar>
|
||||
<div>
|
||||
<button
|
||||
title="Add new"
|
||||
className="k-button k-primary"
|
||||
onClick={addRecord}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</GridToolbar>
|
||||
<GridColumn field="productID" title="Id" width="100px" editable={false} filterable={false} />
|
||||
<GridColumn field="productName" title="Name" />
|
||||
<GridColumn field="category.categoryName" title="Category" cell={DropDownCell} />
|
||||
<GridColumn field="firstOrderedOn" title="First Ordered On" editor="date" filter='date' format={'{0:d}'} />
|
||||
<GridColumn
|
||||
field="unitsInStock"
|
||||
title="Units"
|
||||
width="150px"
|
||||
editor="numeric"
|
||||
filter="numeric"
|
||||
/>
|
||||
<GridColumn field="discontinued" title="Discontinued" editor="boolean" filter="boolean" />
|
||||
<GridColumn width="200px" cell={MyCommandCell} />
|
||||
</Grid>
|
||||
</DataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export { Home };
|
|
@ -0,0 +1,18 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Container } from 'reactstrap';
|
||||
import { NavMenu } from './NavMenu';
|
||||
|
||||
export class Layout extends Component {
|
||||
static displayName = Layout.name;
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavMenu />
|
||||
<Container>
|
||||
{this.props.children}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
a.navbar-brand {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './NavMenu.css';
|
||||
|
||||
export class NavMenu extends Component {
|
||||
static displayName = NavMenu.name;
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.toggleNavbar = this.toggleNavbar.bind(this);
|
||||
this.state = {
|
||||
collapsed: true
|
||||
};
|
||||
}
|
||||
|
||||
toggleNavbar () {
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<header>
|
||||
<Navbar className="navbar-expand-sm navbar-toggleable-sm ng-white border-bottom box-shadow mb-3" light>
|
||||
<Container>
|
||||
<NavbarBrand tag={Link} to="/">kendo_react_grid_crud</NavbarBrand>
|
||||
<NavbarToggler onClick={this.toggleNavbar} className="mr-2" />
|
||||
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={!this.state.collapsed} navbar>
|
||||
<ul className="navbar-nav flex-grow">
|
||||
<NavItem>
|
||||
<NavLink tag={Link} className="text-dark" to="/">Home</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} className="text-dark" to="/counter">Counter</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} className="text-dark" to="/fetch-data">Fetch data</NavLink>
|
||||
</NavItem>
|
||||
</ul>
|
||||
</Collapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import * as React from "react";
|
||||
import DataContext from '../../contexts/data-context';
|
||||
|
||||
const MyCommandCell = props => {
|
||||
const currentContext = React.useContext(DataContext);
|
||||
|
||||
const { dataItem } = props;
|
||||
const isNewItem = dataItem.productID === null;
|
||||
|
||||
const inEdit = dataItem.inEdit;
|
||||
|
||||
const handleAddUpdate = React.useCallback(() =>{
|
||||
if(isNewItem){
|
||||
currentContext.add(dataItem)
|
||||
} else {
|
||||
currentContext.update(dataItem)
|
||||
}
|
||||
},[currentContext, dataItem, isNewItem])
|
||||
|
||||
const handleDiscardCancel = React.useCallback(()=>{
|
||||
isNewItem ? currentContext.discard(dataItem) : currentContext.cancel()
|
||||
},[currentContext, dataItem, isNewItem])
|
||||
|
||||
const handleEdit = React.useCallback(()=> {
|
||||
currentContext.enterEdit(dataItem)
|
||||
},[currentContext, dataItem])
|
||||
|
||||
const handleDelete = React.useCallback(()=> {
|
||||
window.confirm("Confirm deleting: " + dataItem.productName) && currentContext.remove(dataItem)
|
||||
}, [currentContext, dataItem])
|
||||
|
||||
/**
|
||||
* Return null if this is a group header cell.
|
||||
*/
|
||||
|
||||
if(props.rowType === 'groupHeader') return null;
|
||||
|
||||
return inEdit ?
|
||||
(<td className="k-command-cell">
|
||||
<button className="k-button k-grid-save-command" onClick={handleAddUpdate}>
|
||||
{isNewItem ? "Add" : "Update"}
|
||||
</button>
|
||||
<button className="k-button k-grid-cancel-command" onClick={handleDiscardCancel}>
|
||||
{isNewItem ? "Discard" : "Cancel"}
|
||||
</button>
|
||||
</td>) :
|
||||
(<td className="k-command-cell">
|
||||
<button className="k-primary k-button k-grid-edit-command" onClick={handleEdit}>
|
||||
Edit
|
||||
</button>
|
||||
<button className="k-button k-grid-remove-command" onClick={handleDelete}>
|
||||
Remove
|
||||
</button>
|
||||
</td>);
|
||||
};
|
||||
|
||||
export default MyCommandCell;
|
|
@ -0,0 +1,78 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
DropDownList,
|
||||
} from '@progress/kendo-react-dropdowns';
|
||||
|
||||
// This data can also be passed from the Context if it is available in the main component.
|
||||
const categoryData = [
|
||||
{
|
||||
categoryID: 1,
|
||||
categoryName: 'Beverages'
|
||||
},
|
||||
{
|
||||
categoryID: 2,
|
||||
categoryName: 'Condiments'
|
||||
},
|
||||
{
|
||||
categoryID: 6,
|
||||
categoryName: 'Meat/Poultry'
|
||||
},
|
||||
{
|
||||
categoryID: 7,
|
||||
categoryName: 'Produce'
|
||||
},
|
||||
{
|
||||
categoryID: 8,
|
||||
categoryName: 'Seafood'
|
||||
},
|
||||
];
|
||||
|
||||
const DropDownCell = (props) => {
|
||||
|
||||
/**
|
||||
* Return null if this is a group header cell.
|
||||
*/
|
||||
if (props.rowType === 'groupHeader') return null;
|
||||
|
||||
/**
|
||||
* Get both keys for a complex field like Category.CategoryName.
|
||||
*/
|
||||
let fieldComplex = props.field.split('.');
|
||||
|
||||
const handleChange = (e) => {
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
dataIndex: 0,
|
||||
dataItem: props.dataItem,
|
||||
field: fieldComplex[0],
|
||||
syntheticEvent: e.syntheticEvent,
|
||||
value: e.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { dataItem } = props;
|
||||
const dataValue =
|
||||
dataItem[fieldComplex[0]] === null || dataItem[fieldComplex[0]][fieldComplex[1]] === null
|
||||
? ''
|
||||
: dataItem[fieldComplex[0]][fieldComplex[1]];
|
||||
|
||||
return (
|
||||
<td>
|
||||
{dataItem.inEdit ? (
|
||||
<DropDownList
|
||||
style={{ width: '100%' }}
|
||||
onChange={handleChange}
|
||||
value={dataItem[fieldComplex[0]]}
|
||||
data={categoryData}
|
||||
textField={fieldComplex[1]}
|
||||
defaultItem={{ categoryID: 0, categoryName: 'Choose Category' }}
|
||||
/>
|
||||
) : (
|
||||
dataValue.toString()
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropDownCell;
|
|
@ -0,0 +1,12 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
const DataContext = createContext({
|
||||
enterEdit: () => {},
|
||||
remove: () => {},
|
||||
add: () => {},
|
||||
discard: () => {},
|
||||
update: () => {},
|
||||
cancel: () => {}
|
||||
});
|
||||
|
||||
export default DataContext;
|
|
@ -0,0 +1,14 @@
|
|||
/* Provide sufficient contrast against white background */
|
||||
a {
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #E01A76;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
ReactDOM.render(
|
||||
<BrowserRouter basename={baseUrl}>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
rootElement);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://cra.link/PWA
|
||||
serviceWorkerRegistration.unregister();
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
|
@ -0,0 +1,13 @@
|
|||
const reportWebVitals = (onPerfEntry) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
|
@ -0,0 +1,72 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
// This service worker can be customized!
|
||||
// See https://developers.google.com/web/tools/workbox/modules
|
||||
// for the list of available Workbox modules, or add any other
|
||||
// code you'd like.
|
||||
// You can also remove this file if you'd prefer not to use a
|
||||
// service worker, and the Workbox build step will be skipped.
|
||||
|
||||
import { clientsClaim } from 'workbox-core';
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
|
||||
import { registerRoute } from 'workbox-routing';
|
||||
import { StaleWhileRevalidate } from 'workbox-strategies';
|
||||
|
||||
clientsClaim();
|
||||
|
||||
// Precache all of the assets generated by your build process.
|
||||
// Their URLs are injected into the manifest variable below.
|
||||
// This variable must be present somewhere in your service worker file,
|
||||
// even if you decide not to use precaching. See https://cra.link/PWA
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
// Set up App Shell-style routing, so that all navigation requests
|
||||
// are fulfilled with your index.html shell. Learn more at
|
||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
|
||||
registerRoute(
|
||||
// Return false to exempt requests from being fulfilled by index.html.
|
||||
({ request, url }) => {
|
||||
// If this isn't a navigation, skip.
|
||||
if (request.mode !== 'navigate') {
|
||||
return false;
|
||||
} // If this is a URL that starts with /_, skip.
|
||||
|
||||
if (url.pathname.startsWith('/_')) {
|
||||
return false;
|
||||
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
||||
|
||||
if (url.pathname.match(fileExtensionRegexp)) {
|
||||
return false;
|
||||
} // Return true to signal that we want to use the handler.
|
||||
|
||||
return true;
|
||||
},
|
||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||
);
|
||||
|
||||
// An example runtime caching route for requests that aren't handled by the
|
||||
// precache, in this case same-origin .png requests like those from in public/
|
||||
registerRoute(
|
||||
// Add in any other file extensions or routing criteria as needed.
|
||||
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'images',
|
||||
plugins: [
|
||||
// Ensure that once this runtime cache reaches a maximum size the
|
||||
// least-recently used images are removed.
|
||||
new ExpirationPlugin({ maxEntries: 50 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// This allows the web app to trigger skipWaiting via
|
||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
// Any other custom service worker logic can go here.
|
|
@ -0,0 +1,137 @@
|
|||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://cra.link/PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://cra.link/PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('No internet connection found. App is running in offline mode.');
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
const createProxyMiddleware = require('http-proxy-middleware');
|
||||
const { env } = require('process');
|
||||
|
||||
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
|
||||
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:58261';
|
||||
|
||||
const context = [
|
||||
"/products",
|
||||
];
|
||||
|
||||
module.exports = function(app) {
|
||||
const appProxy = createProxyMiddleware(context, {
|
||||
target: target,
|
||||
secure: false
|
||||
});
|
||||
|
||||
app.use(appProxy);
|
||||
};
|
|
@ -0,0 +1,132 @@
|
|||
using Kendo.Mvc;
|
||||
using Kendo.Mvc.Examples.Models;
|
||||
using Kendo.Mvc.Extensions;
|
||||
using Kendo.Mvc.Infrastructure;
|
||||
using Kendo.Mvc.UI;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace kendo_react_grid_crud.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class ProductsController : Controller
|
||||
{
|
||||
private static ProductViewModel[] ProductViewModelData;
|
||||
public class PostModel
|
||||
{
|
||||
public ProductViewModel? Product { get; set; }
|
||||
public int Take { get; set; }
|
||||
public int Skip { get; set; }
|
||||
public string? Sort { get; set; }
|
||||
public string? Filter { get; set; }
|
||||
public string? Group { get; set; }
|
||||
}
|
||||
|
||||
private readonly ILogger<ProductsController> _logger;
|
||||
|
||||
public ProductsController(ILogger<ProductsController> logger)
|
||||
{
|
||||
if (ProductViewModelData == null) {
|
||||
ProductViewModelData = new ProductViewModel[]
|
||||
{
|
||||
new ProductViewModel(){ProductID = 1, ProductName = "Chai", FirstOrderedOn = DateTime.Today, UnitsInStock = 18, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 1, CategoryName = "Beverages"} },
|
||||
new ProductViewModel(){ProductID = 2, ProductName = "Chang", FirstOrderedOn = DateTime.Today, UnitsInStock = 19, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 1, CategoryName = "Beverages"} },
|
||||
new ProductViewModel(){ProductID = 3, ProductName = "Aniseed Syrup", FirstOrderedOn = DateTime.Today, UnitsInStock = 10, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 2, CategoryName = "Condiments"} },
|
||||
new ProductViewModel(){ProductID = 4, ProductName = "Chef Anton's Cajun Seasoning", FirstOrderedOn = DateTime.Today, UnitsInStock = 22, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 2, CategoryName = "Condiments"} },
|
||||
new ProductViewModel(){ProductID = 5, ProductName = "Chef Anton's Gumbo Mix", FirstOrderedOn = DateTime.Today, UnitsInStock = 23, Discontinued = true, Category = new CategoryViewModel(){ CategoryID = 2, CategoryName = "Condiments"} },
|
||||
new ProductViewModel(){ProductID = 6, ProductName = "Grandma's Boysenberry Spread", FirstOrderedOn = DateTime.Today, UnitsInStock = 25, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 2, CategoryName = "Condiments"} },
|
||||
new ProductViewModel(){ProductID = 7, ProductName = "Uncle Bob's Organic Dried Pears", FirstOrderedOn = DateTime.Today, UnitsInStock = 30, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 7, CategoryName = "Produce"} },
|
||||
new ProductViewModel(){ProductID = 8, ProductName = "Northwoods Cranberry Sauce", FirstOrderedOn = DateTime.Today, UnitsInStock = 40, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 2, CategoryName = "Condiments"} },
|
||||
new ProductViewModel(){ProductID = 9, ProductName = "Mishi Kobe Niku", FirstOrderedOn = DateTime.Today, UnitsInStock = 97, Discontinued = true, Category = new CategoryViewModel(){ CategoryID = 6, CategoryName = "Meat/Poultry"} },
|
||||
new ProductViewModel(){ProductID = 10, ProductName = "Ikura", FirstOrderedOn = DateTime.Today, UnitsInStock = 31, Discontinued = false, Category = new CategoryViewModel(){ CategoryID = 8, CategoryName = "Seafood"} },
|
||||
};
|
||||
}
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<ProductViewModel> Get()
|
||||
{
|
||||
return ProductViewModelData.ToArray();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public DataSourceResult Post([DataSourceRequest] DataSourceRequest request)
|
||||
{
|
||||
return ProductViewModelData.ToDataSourceResult(request);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public JsonResult Put([FromBody] PostModel postModel)
|
||||
{
|
||||
var dsRequest = new DataSourceRequest();
|
||||
dsRequest.Take = postModel.Take;
|
||||
dsRequest.Skip = postModel.Skip;
|
||||
dsRequest.PageSize = postModel.Take;
|
||||
// we check if this is an existing or a new item
|
||||
if(postModel.Product != null && postModel.Product.ProductID != null)
|
||||
{
|
||||
// We update the local data, but in a real Application here is the place to update the database;
|
||||
var productToBeChanged = ProductViewModelData.Where(x => x.ProductID == postModel.Product.ProductID).FirstOrDefault();
|
||||
productToBeChanged.ProductName = postModel.Product.ProductName;
|
||||
productToBeChanged.Discontinued = postModel.Product.Discontinued;
|
||||
productToBeChanged.Category = postModel.Product.Category;
|
||||
productToBeChanged.UnitsInStock = postModel.Product.UnitsInStock;
|
||||
productToBeChanged.FirstOrderedOn = postModel.Product.FirstOrderedOn;
|
||||
} else
|
||||
{
|
||||
postModel.Product.ProductID = DateTime.Now.Millisecond;
|
||||
ProductViewModelData = ProductViewModelData.Prepend(postModel.Product).ToArray();
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(postModel.Filter))
|
||||
{
|
||||
dsRequest.Filters = FilterDescriptorFactory.Create(postModel.Filter);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(postModel.Sort))
|
||||
{
|
||||
dsRequest.Sorts = DataSourceDescriptorSerializer.Deserialize<SortDescriptor>(postModel.Sort);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(postModel.Group))
|
||||
{
|
||||
dsRequest.Groups = DataSourceDescriptorSerializer.Deserialize<GroupDescriptor>(postModel.Group);
|
||||
}
|
||||
|
||||
var result = Json(ProductViewModelData.ToDataSourceResult(dsRequest));
|
||||
|
||||
return result;
|
||||
}
|
||||
[HttpDelete]
|
||||
public JsonResult Delete([FromBody] PostModel postModel)
|
||||
{
|
||||
var dsRequest = new DataSourceRequest();
|
||||
dsRequest.Take = postModel.Take;
|
||||
dsRequest.Skip = postModel.Skip;
|
||||
dsRequest.PageSize = postModel.Take;
|
||||
// We Delete the local data, but in a real Application here is the place to mark it as detele it the database;
|
||||
ProductViewModelData = ProductViewModelData.Where(val => val.ProductID != postModel.Product.ProductID).ToArray();
|
||||
|
||||
if (!string.IsNullOrEmpty(postModel.Filter))
|
||||
{
|
||||
dsRequest.Filters = FilterDescriptorFactory.Create(postModel.Filter);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(postModel.Sort))
|
||||
{
|
||||
dsRequest.Sorts = DataSourceDescriptorSerializer.Deserialize<SortDescriptor>(postModel.Sort);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(postModel.Group))
|
||||
{
|
||||
dsRequest.Groups = DataSourceDescriptorSerializer.Deserialize<GroupDescriptor>(postModel.Group);
|
||||
}
|
||||
|
||||
var result = Json(ProductViewModelData.ToDataSourceResult(dsRequest));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel;
|
||||
using System;
|
||||
|
||||
namespace Kendo.Mvc.Examples.Models
|
||||
{
|
||||
public class CategoryViewModel
|
||||
{
|
||||
public int CategoryID { get; set; }
|
||||
public string CategoryName { get; set; }
|
||||
}
|
||||
public class ProductViewModel
|
||||
{
|
||||
public int? ProductID
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Product name")]
|
||||
public string? ProductName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Unit price")]
|
||||
public decimal? UnitPrice
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Units in stock")]
|
||||
[DataType("Integer")]
|
||||
public int? UnitsInStock
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool? Discontinued
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "First Ordered On")]
|
||||
[DataType(DataType.Date)]
|
||||
public DateTime FirstOrderedOn
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataType("Integer")]
|
||||
public int? UnitsOnOrder
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public CategoryViewModel? Category
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
|
@ -0,0 +1,26 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace kendo_react_grid_crud.Pages
|
||||
{
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
@using kendo_react_grid_crud
|
||||
@namespace kendo_react_grid_crud.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
@ -0,0 +1,29 @@
|
|||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllersWithViews().AddNewtonsoftJson();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseDefaultFiles();
|
||||
app.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller}/{action=Index}/{id?}");
|
||||
|
||||
app.MapFallbackToFile("index.html"); ;
|
||||
|
||||
app.Run();
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:58261",
|
||||
"sslPort": 44387
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"kendo_react_grid_crud": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7266;http://localhost:5266",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.AspNetCore.SpaProxy": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<SpaRoot>ClientApp\</SpaRoot>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
|
||||
<SpaProxyServerUrl>https://localhost:44487</SpaProxyServerUrl>
|
||||
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
|
||||
<RootNamespace>kendo_react_grid_crud</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="6.0.0" />
|
||||
<PackageReference Include="Telerik.DataSource" Version="2.1.0" />
|
||||
<PackageReference Include="Telerik.UI.for.AspNet.Core" Version="2021.3.1109" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
||||
<Content Remove="$(SpaRoot)**" />
|
||||
<None Remove="$(SpaRoot)**" />
|
||||
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
||||
<!-- Ensure Node.js is installed -->
|
||||
<Exec Command="node --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
||||
</Exec>
|
||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
|
||||
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
|
||||
|
||||
<!-- Include the newly-built files in the publish output -->
|
||||
<ItemGroup>
|
||||
<DistFiles Include="$(SpaRoot)build\**" />
|
||||
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
||||
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
Загрузка…
Ссылка в новой задаче