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 {
editIcon: 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` */
locals: StyleModuleCssModule.IStyleModuleCss;
locals: StyleModuleCssNamespace.IStyleModuleCss;
};
export = StyleModuleCssModule;

View File

@ -1,12 +1,12 @@
declare namespace StyleModuleCssModule {
declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss {
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` */
locals: StyleModuleCssModule.IStyleModuleCss;
locals: StyleModuleCssNamespace.IStyleModuleCss;
};
export = StyleModuleCssModule;

View File

@ -1,12 +1,12 @@
declare namespace StyleModuleCssModule {
declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss {
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` */
locals: StyleModuleCssModule.IStyleModuleCss;
locals: StyleModuleCssNamespace.IStyleModuleCss;
};
export = StyleModuleCssModule;

View File

@ -1,13 +1,13 @@
declare namespace StyleModuleCssModule {
declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss {
tabContent: 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` */
locals: StyleModuleCssModule.IStyleModuleCss;
locals: StyleModuleCssNamespace.IStyleModuleCss;
};
export = StyleModuleCssModule;

View File

@ -1,13 +1,13 @@
declare namespace StyleModuleCssModule {
declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss {
home: 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` */
locals: StyleModuleCssModule.IStyleModuleCss;
locals: StyleModuleCssNamespace.IStyleModuleCss;
};
export = StyleModuleCssModule;

View File

@ -1,12 +1,12 @@
declare namespace StyleModuleCssModule {
declare namespace StyleModuleCssNamespace {
export interface IStyleModuleCss {
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` */
locals: StyleModuleCssModule.IStyleModuleCss;
locals: StyleModuleCssNamespace.IStyleModuleCss;
};
export = StyleModuleCssModule;

View File

@ -8,6 +8,8 @@ import { useProjectReducer, addTask, updateTaskEstimation, removeTask, updateTas
import { Task, TaskID, EstimationConfidence } from "../../models/task";
import TimePreview from "../project/time-preview";
import RepartitionPreview from "../project/repartition-preview";
import FinancialPreview from "../project/financial-preview";
import { getHideFinancialPreviewOnPrint } from "../../models/params";
export interface PdfProps {
projectId: string
@ -20,14 +22,21 @@ const Pdf: FunctionalComponent<PdfProps> = ({ projectId }) => {
return (
<div class={`container ${style.pdf}`}>
<div class="column is-9">
<TaskTable
project={project}
readonly={true} />
<div class="columns">
<div class="column is-9">
<TaskTable
project={project}
readonly={true} />
</div>
<div class="column is-3">
<TimePreview project={project} />
<RepartitionPreview project={project} />
</div>
</div>
<div class="column is-3">
<TimePreview project={project} />
<RepartitionPreview project={project} />
<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 {
estimation: 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` */
locals: StyleModuleCssModule.IStyleModuleCss;
locals: StyleModuleCssNamespace.IStyleModuleCss;
};
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}`}>
<thead>
<tr>
{
readonly ? '' :
<th class={`${style.noBorder} noPrint`} rowSpan={2}></th>
}
<th class={style.mainColumn} rowSpan={2}>Tâche</th>
<th rowSpan={2}>Catégorie</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 : '???';
return (
<tr key={`taks-${t.id}`}>
{
readonly ? '' :
<td class={`is-narrow noPrint`}>
<button
onClick={onTaskRemoveClick.bind(null, t.id)}
@ -116,6 +121,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
🗑
</button>
</td>
}
<td class={style.mainColumn}>
<EditableText
render={(value) => (<span>{value}</span>)}
@ -166,7 +172,9 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
<tfoot>
<tr>
<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">
<p class="control is-expanded">
<input class="input" type="text" placeholder="Nouvelle tâche"
@ -191,6 +199,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
</a>
</p>
</div>
}
</td>
<th colSpan={3}>Total</th>
</tr>

View File

@ -27,6 +27,9 @@ module.exports = {
proxy: {
'/api': {
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
require (
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.5.0
github.com/asdine/storm/v3 v3.1.1
github.com/caarlos0/env/v6 v6.2.1
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/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
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/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
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"
)
// Config is the configuration struct
type Config struct {
HTTP HTTPConfig `yaml:"http"`
Data DataConfig `ymal:"data"`
HTTP HTTPConfig `yaml:"http"`
Client ClientConfig `yaml:"client"`
Data DataConfig `ymal:"data"`
}
// HTTPConfig is the configuration part which defines HTTP related params
type HTTPConfig struct {
BaseURL string `yaml:"baseurl" env:"GUESSTIMATE_BASE_URL"`
Address string `yaml:"address" env:"GUESSTIMATE_HTTP_ADDRESS"`
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 {
Path string `yaml:"path" env:"GUESSTIMATE_DATA_PATH"`
}
@ -40,6 +51,7 @@ func NewFromFile(filepath string) (*Config, error) {
return config, nil
}
// WithEnvironment retrieves the configuration from env vars
func WithEnvironment(conf *Config) error {
if err := env.Parse(conf); err != nil {
return err
@ -48,23 +60,31 @@ func WithEnvironment(conf *Config) error {
return nil
}
// NewDumpDefault retrieves the default configuration
func NewDumpDefault() *Config {
config := NewDefault()
return config
}
// NewDefault creates and returns a new default configuration
func NewDefault() *Config {
return &Config{
HTTP: HTTPConfig{
BaseURL: "localhost",
Address: ":8081",
PublicDir: "public",
},
Client: ClientConfig{
BaseURL: "localhost",
Address: ":8080",
},
Data: DataConfig{
Path: "guesstimate.db",
},
}
}
// Dump writes a given config to a config file
func Dump(config *Config, w io.Writer) error {
data, err := yaml.Marshal(config)
if err != nil {

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import (
"gitlab.com/wpetit/goweb/static"
)
// Mount endoints for server app
func Mount(r *chi.Mux, config *config.Config) error {
r.Route("/api/v1", func(r chi.Router) {
r.Get("/projects/{projectID}", handleGetProject)
@ -17,6 +18,10 @@ func Mount(r *chi.Mux, config *config.Config) error {
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")
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("/pdf/*", serveClientIndex)
notFoundHandler := r.NotFoundHandler()
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler))

View File

@ -2,14 +2,17 @@ package route
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
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/storm"
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"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 {
Project *model.Project `json:"project"`
}
@ -246,3 +305,12 @@ func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) error {
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)
}