Création d'une page tableau en lecture seule et possibilité d'exporter en PDF via wkhtmltopdf

This commit is contained in:
Teddy Cornaut 2020-06-15 15:05:28 -04:00
parent 3c21412344
commit 6ebe4c90d7
17 changed files with 151 additions and 31 deletions

View File

@ -1,13 +1,13 @@
declare namespace StyleModuleCssModule { declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss { export interface IStyleModuleCss {
editIcon: string; editIcon: string;
editableText: string; editableText: string;
} }
} }
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & { declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: StyleModuleCssModule.IStyleModuleCss; locals: StyleModuleCssNamespace.IStyleModuleCss;
}; };
export = StyleModuleCssModule; export = StyleModuleCssModule;

View File

@ -1,12 +1,12 @@
declare namespace StyleModuleCssModule { declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss { export interface IStyleModuleCss {
footer: string; footer: string;
} }
} }
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & { declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: StyleModuleCssModule.IStyleModuleCss; locals: StyleModuleCssNamespace.IStyleModuleCss;
}; };
export = StyleModuleCssModule; export = StyleModuleCssModule;

View File

@ -1,12 +1,12 @@
declare namespace StyleModuleCssModule { declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss { export interface IStyleModuleCss {
header: string; header: string;
} }
} }
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & { declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: StyleModuleCssModule.IStyleModuleCss; locals: StyleModuleCssNamespace.IStyleModuleCss;
}; };
export = StyleModuleCssModule; export = StyleModuleCssModule;

View File

@ -1,13 +1,13 @@
declare namespace StyleModuleCssModule { declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss { export interface IStyleModuleCss {
tabContent: string; tabContent: string;
tabs: string; tabs: string;
} }
} }
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & { declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: StyleModuleCssModule.IStyleModuleCss; locals: StyleModuleCssNamespace.IStyleModuleCss;
}; };
export = StyleModuleCssModule; export = StyleModuleCssModule;

View File

@ -1,13 +1,13 @@
declare namespace StyleModuleCssModule { declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss { export interface IStyleModuleCss {
home: string; home: string;
noProjects: string; noProjects: string;
} }
} }
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & { declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: StyleModuleCssModule.IStyleModuleCss; locals: StyleModuleCssNamespace.IStyleModuleCss;
}; };
export = StyleModuleCssModule; export = StyleModuleCssModule;

View File

@ -1,12 +1,12 @@
declare namespace StyleModuleCssModule { declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss { export interface IStyleModuleCss {
notFound: string; notFound: string;
} }
} }
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & { declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: StyleModuleCssModule.IStyleModuleCss; locals: StyleModuleCssNamespace.IStyleModuleCss;
}; };
export = StyleModuleCssModule; export = StyleModuleCssModule;

View File

@ -8,6 +8,8 @@ import { useProjectReducer, addTask, updateTaskEstimation, removeTask, updateTas
import { Task, TaskID, EstimationConfidence } from "../../models/task"; import { Task, TaskID, EstimationConfidence } from "../../models/task";
import TimePreview from "../project/time-preview"; import TimePreview from "../project/time-preview";
import RepartitionPreview from "../project/repartition-preview"; import RepartitionPreview from "../project/repartition-preview";
import FinancialPreview from "../project/financial-preview";
import { getHideFinancialPreviewOnPrint } from "../../models/params";
export interface PdfProps { export interface PdfProps {
projectId: string projectId: string
@ -20,6 +22,7 @@ const Pdf: FunctionalComponent<PdfProps> = ({ projectId }) => {
return ( return (
<div class={`container ${style.pdf}`}> <div class={`container ${style.pdf}`}>
<div class="columns">
<div class="column is-9"> <div class="column is-9">
<TaskTable <TaskTable
project={project} project={project}
@ -30,6 +33,12 @@ const Pdf: FunctionalComponent<PdfProps> = ({ projectId }) => {
<RepartitionPreview project={project} /> <RepartitionPreview project={project} />
</div> </div>
</div> </div>
<div class="columns">
<div class={`column ${getHideFinancialPreviewOnPrint(project) ? 'noPrint': ''}`}>
<FinancialPreview project={project} />
</div>
</div>
</div>
); );
}; };

View File

@ -1,4 +1,4 @@
declare namespace StyleModuleCssModule { declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss { export interface IStyleModuleCss {
estimation: string; estimation: string;
mainColumn: string; mainColumn: string;
@ -9,9 +9,9 @@ declare namespace StyleModuleCssModule {
} }
} }
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & { declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: StyleModuleCssModule.IStyleModuleCss; locals: StyleModuleCssNamespace.IStyleModuleCss;
}; };
export = StyleModuleCssModule; export = StyleModuleCssModule;

View File

@ -91,7 +91,10 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
<table class={`table is-bordered is-striped is-hoverable is-fullwidth ${style.middleTable}`}> <table class={`table is-bordered is-striped is-hoverable is-fullwidth ${style.middleTable}`}>
<thead> <thead>
<tr> <tr>
{
readonly ? '' :
<th class={`${style.noBorder} noPrint`} rowSpan={2}></th> <th class={`${style.noBorder} noPrint`} rowSpan={2}></th>
}
<th class={style.mainColumn} rowSpan={2}>Tâche</th> <th class={style.mainColumn} rowSpan={2}>Tâche</th>
<th rowSpan={2}>Catégorie</th> <th rowSpan={2}>Catégorie</th>
<th colSpan={3}>Estimation (en <ProjectTimeUnit project={project} />)</th> <th colSpan={3}>Estimation (en <ProjectTimeUnit project={project} />)</th>
@ -109,6 +112,8 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
const categoryLabel = category ? category.label : '???'; const categoryLabel = category ? category.label : '???';
return ( return (
<tr key={`taks-${t.id}`}> <tr key={`taks-${t.id}`}>
{
readonly ? '' :
<td class={`is-narrow noPrint`}> <td class={`is-narrow noPrint`}>
<button <button
onClick={onTaskRemoveClick.bind(null, t.id)} onClick={onTaskRemoveClick.bind(null, t.id)}
@ -116,6 +121,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
🗑 🗑
</button> </button>
</td> </td>
}
<td class={style.mainColumn}> <td class={style.mainColumn}>
<EditableText <EditableText
render={(value) => (<span>{value}</span>)} render={(value) => (<span>{value}</span>)}
@ -166,7 +172,9 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
<tfoot> <tfoot>
<tr> <tr>
<td class={`${style.noBorder} noPrint`}></td> <td class={`${style.noBorder} noPrint`}></td>
<td colSpan={2} class={readonly ? style.noBorder : ''}> <td colSpan={readonly ? 1 : 2} class={readonly ? style.noBorder : ''}>
{
readonly ? '' :
<div class="field has-addons noPrint"> <div class="field has-addons noPrint">
<p class="control is-expanded"> <p class="control is-expanded">
<input class="input" type="text" placeholder="Nouvelle tâche" <input class="input" type="text" placeholder="Nouvelle tâche"
@ -191,6 +199,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
</a> </a>
</p> </p>
</div> </div>
}
</td> </td>
<th colSpan={3}>Total</th> <th colSpan={3}>Total</th>
</tr> </tr>

View File

@ -27,6 +27,9 @@ module.exports = {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://127.0.0.1:8081', target: 'http://127.0.0.1:8081',
},
'/export': {
target: 'http://127.0.0.1:8081',
} }
} }
}, },

View File

@ -3,6 +3,7 @@ module forge.cadoles.com/wpetit/guesstimate
go 1.14 go 1.14
require ( require (
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.5.0
github.com/asdine/storm/v3 v3.1.1 github.com/asdine/storm/v3 v3.1.1
github.com/caarlos0/env/v6 v6.2.1 github.com/caarlos0/env/v6 v6.2.1
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect

View File

@ -19,6 +19,8 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.5.0 h1:KNMkSqhko6c7eVncl/laVCS95jRryULVhVwwL0VynnU=
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.5.0/go.mod h1:zRBvVJtIfhaWvQ9lPIkXrtona4qqSmjZ1HfKvq4dQzI=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=

View File

@ -10,16 +10,27 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// Config is the configuration struct
type Config struct { type Config struct {
HTTP HTTPConfig `yaml:"http"` HTTP HTTPConfig `yaml:"http"`
Client ClientConfig `yaml:"client"`
Data DataConfig `ymal:"data"` Data DataConfig `ymal:"data"`
} }
// HTTPConfig is the configuration part which defines HTTP related params
type HTTPConfig struct { type HTTPConfig struct {
BaseURL string `yaml:"baseurl" env:"GUESSTIMATE_BASE_URL"`
Address string `yaml:"address" env:"GUESSTIMATE_HTTP_ADDRESS"` Address string `yaml:"address" env:"GUESSTIMATE_HTTP_ADDRESS"`
PublicDir string `yaml:"publicDir" env:"GUESSTIMATE_PUBLIC_DIR"` PublicDir string `yaml:"publicDir" env:"GUESSTIMATE_PUBLIC_DIR"`
} }
// ClientConfig is the configuration part which defines the client app related params
type ClientConfig struct {
BaseURL string `yaml:"baseurl" env:"GUESSTIMATE_CLIENT_BASE_URL"`
Address string `yaml:"address" env:"GUESSTIMATE_CLIENT_HTTP_ADDRESS"`
}
// DataConfig is the configuration part which defines data related params
type DataConfig struct { type DataConfig struct {
Path string `yaml:"path" env:"GUESSTIMATE_DATA_PATH"` Path string `yaml:"path" env:"GUESSTIMATE_DATA_PATH"`
} }
@ -40,6 +51,7 @@ func NewFromFile(filepath string) (*Config, error) {
return config, nil return config, nil
} }
// WithEnvironment retrieves the configuration from env vars
func WithEnvironment(conf *Config) error { func WithEnvironment(conf *Config) error {
if err := env.Parse(conf); err != nil { if err := env.Parse(conf); err != nil {
return err return err
@ -48,23 +60,31 @@ func WithEnvironment(conf *Config) error {
return nil return nil
} }
// NewDumpDefault retrieves the default configuration
func NewDumpDefault() *Config { func NewDumpDefault() *Config {
config := NewDefault() config := NewDefault()
return config return config
} }
// NewDefault creates and returns a new default configuration
func NewDefault() *Config { func NewDefault() *Config {
return &Config{ return &Config{
HTTP: HTTPConfig{ HTTP: HTTPConfig{
BaseURL: "localhost",
Address: ":8081", Address: ":8081",
PublicDir: "public", PublicDir: "public",
}, },
Client: ClientConfig{
BaseURL: "localhost",
Address: ":8080",
},
Data: DataConfig{ Data: DataConfig{
Path: "guesstimate.db", Path: "guesstimate.db",
}, },
} }
} }
// Dump writes a given config to a config file
func Dump(config *Config, w io.Writer) error { func Dump(config *Config, w io.Writer) error {
data, err := yaml.Marshal(config) data, err := yaml.Marshal(config)
if err != nil { if err != nil {

View File

@ -2,6 +2,7 @@ package config
import "gitlab.com/wpetit/goweb/service" import "gitlab.com/wpetit/goweb/service"
// ServiceProvider returns the current config service
func ServiceProvider(config *Config) service.Provider { func ServiceProvider(config *Config) service.Provider {
return func(ctn *service.Container) (interface{}, error) { return func(ctn *service.Container) (interface{}, error) {
return config, nil return config, nil

View File

@ -5,6 +5,7 @@ import (
"gitlab.com/wpetit/goweb/service" "gitlab.com/wpetit/goweb/service"
) )
// ServiceName defines the project's service
const ServiceName service.Name = "config" const ServiceName service.Name = "config"
// From retrieves the config service in the given container // From retrieves the config service in the given container

View File

@ -9,6 +9,7 @@ import (
"gitlab.com/wpetit/goweb/static" "gitlab.com/wpetit/goweb/static"
) )
// Mount endoints for server app
func Mount(r *chi.Mux, config *config.Config) error { func Mount(r *chi.Mux, config *config.Config) error {
r.Route("/api/v1", func(r chi.Router) { r.Route("/api/v1", func(r chi.Router) {
r.Get("/projects/{projectID}", handleGetProject) r.Get("/projects/{projectID}", handleGetProject)
@ -17,6 +18,10 @@ func Mount(r *chi.Mux, config *config.Config) error {
r.Delete("/projects/{projectID}", handleDeleteProject) r.Delete("/projects/{projectID}", handleDeleteProject)
}) })
r.Route("/export", func(r chi.Router) {
r.Get("/projects/{projectID}", handleExportProject)
})
clientIndex := path.Join(config.HTTP.PublicDir, "index.html") clientIndex := path.Join(config.HTTP.PublicDir, "index.html")
serveClientIndex := func(w http.ResponseWriter, r *http.Request) { serveClientIndex := func(w http.ResponseWriter, r *http.Request) {
@ -24,6 +29,7 @@ func Mount(r *chi.Mux, config *config.Config) error {
} }
r.Get("/p/*", serveClientIndex) r.Get("/p/*", serveClientIndex)
r.Get("/pdf/*", serveClientIndex)
notFoundHandler := r.NotFoundHandler() notFoundHandler := r.NotFoundHandler()
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler)) r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler))

View File

@ -2,14 +2,17 @@ package route
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
jsonpatch "gopkg.in/evanphx/json-patch.v4" jsonpatch "gopkg.in/evanphx/json-patch.v4"
"forge.cadoles.com/wpetit/guesstimate/internal/config"
"forge.cadoles.com/wpetit/guesstimate/internal/model" "forge.cadoles.com/wpetit/guesstimate/internal/model"
"forge.cadoles.com/wpetit/guesstimate/internal/storm" "forge.cadoles.com/wpetit/guesstimate/internal/storm"
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container" "gitlab.com/wpetit/goweb/middleware/container"
@ -74,6 +77,62 @@ func handleGetProject(w http.ResponseWriter, r *http.Request) {
} }
} }
func handleExportProject(w http.ResponseWriter, r *http.Request) {
ctn := container.Must(r.Context())
cfg := config.Must(ctn)
projectID := getProjectID(r)
var (
err error
url string
)
url = "http://" + string(cfg.Client.BaseURL) + string(cfg.Client.Address) + "/pdf/" + string(projectID)
fmt.Println(url)
// Create new PDF generator
pdfg, err := wkhtmltopdf.NewPDFGenerator()
if err != nil {
log.Fatal(err)
}
// Set global options
pdfg.Dpi.Set(300)
pdfg.Orientation.Set(wkhtmltopdf.OrientationPortrait)
pdfg.Grayscale.Set(true)
// Create a new input page from an URL
page := wkhtmltopdf.NewPage(url)
// Set options for this page
page.FooterRight.Set("[page]")
page.FooterFontSize.Set(10)
page.Zoom.Set(0.95)
// Add to document
pdfg.AddPage(page)
// Create PDF document in internal buffer
err = pdfg.Create()
if err != nil {
log.Fatal(err)
}
// Write buffer contents to file on disk
err = pdfg.WriteFile("./sample.pdf")
if err != nil {
log.Fatal(err)
}
rsp, err := writePDF(w, http.StatusOK, pdfg.Bytes())
if err != nil {
panic(errors.Wrap(err, "could not write pdf response"))
}
fmt.Println(rsp)
}
type createRequest struct { type createRequest struct {
Project *model.Project `json:"project"` Project *model.Project `json:"project"`
} }
@ -246,3 +305,12 @@ func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) error {
return encoder.Encode(data) return encoder.Encode(data)
} }
func writePDF(w http.ResponseWriter, statusCode int, data []byte) (int, error) {
w.Header().Set("Content-Disposition", "attachment; filename=foo.pdf")
w.Header().Set("Content-Type", "application/pdf")
w.WriteHeader(statusCode)
return w.Write(data)
}