Skip to content

Commit

Permalink
add UserChart / UserQuery widgets to TemplateControls
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasM1978 committed Nov 3, 2021
1 parent e229ecc commit e8d4ab2
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 41 deletions.
11 changes: 1 addition & 10 deletions Signum.React.Extensions/Mailing/Templates/EmailTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,21 +233,12 @@ export function EmailTemplateMessageComponent(p: EmailTemplateMessageComponentPr
forceUpdate();
}

function handleOnInsert(newCode: string) {
ValueLineModal.show({
type: { name: "string" },
initialValue: newCode,
title: "Template",
message: "Copy to clipboard: Ctrl+C, ESC",
initiallyFocused: true,
}).done();
}
const ec = p.ctx.subCtx({ labelColumns: { sm: 2 } });
return (
<div className="sf-email-template-message">
<EntityCombo ctx={ec.subCtx(e => e.cultureInfo)} labelText={EmailTemplateViewMessage.Language.niceToString()} onChange={p.invalidate} />
<div>
<TemplateControls queryKey={p.queryKey} onInsert={handleOnInsert} forHtml={true} />
<TemplateControls queryKey={p.queryKey} forHtml={true} />
<ValueLine ctx={ec.subCtx(e => e.subject)} formGroupStyle={"SrOnly"} placeholderLabels={true} labelHtmlAttributes={{ style: { width: "100px" } }} />
<div className="code-container">
<HtmlCodemirror ctx={ec.subCtx(e => e.text)} onChange={handleCodeMirrorChange} />
Expand Down
13 changes: 1 addition & 12 deletions Signum.React.Extensions/SMS/Templates/SMSTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,6 @@ export interface SMSTemplateMessageComponentProps {
export function SMSTemplateMessageComponent(p: SMSTemplateMessageComponentProps) {
const forceUpdate = useForceUpdate();

function handleOnInsert(newCode: string) {
ValueLineModal.show({
type: { name: "string" },
initialValue: newCode,
title: "Template",
message: "Copy to clipboard: Ctrl+C, ESC",
initiallyFocused: true,
}).done();
}


var throttleText = useThrottle(p.ctx.value.message ?? "", 1000);
var remaining = useAPI(abort => SMSClient.API.getRemainingCharacters(throttleText, p.removeNoSMSCharacters), [throttleText, p.removeNoSMSCharacters], { avoidReset: true });

Expand All @@ -77,7 +66,7 @@ export function SMSTemplateMessageComponent(p: SMSTemplateMessageComponentProps)
<div className="sf-sms-template-message">
<EntityCombo ctx={ec.subCtx(e => e.cultureInfo)} onChange={p.invalidate} valueColumns={3} />
<div>
<TemplateControls queryKey={p.queryKey} onInsert={handleOnInsert} forHtml={true} />
<TemplateControls queryKey={p.queryKey} forHtml={true} />
<ValueLine ctx={ec.subCtx(a => a.message)} onChange={forceUpdate} formGroupStyle="SrOnly" formGroupHtmlAttributes={{ className: "pt-2" }} helpText={
<span className={remaining == null ? "" : remaining < 0 ? "text-danger" : remaining < 20 ? "text-warning" : "text-success"}>
{SMSTemplateMessage._0CharactersRemainingBeforeReplacements.niceToString(remaining == null ? "…" : remaining)}
Expand Down
81 changes: 74 additions & 7 deletions Signum.React.Extensions/Templating/TemplateControls.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,59 @@
import * as React from 'react'
import { SubTokensOptions, QueryToken, hasAnyOrAll } from '@framework/FindOptions'
import { SubTokensOptions, QueryToken, hasAnyOrAll, FindOptions } from '@framework/FindOptions'
import { TemplateTokenMessage } from './Signum.Entities.Templating'
import QueryTokenBuilder from '@framework/SearchControl/QueryTokenBuilder'
import ValueLineModal from '../../Signum.React/Scripts/ValueLineModal'
import { UserChartEntity } from '../Chart/Signum.Entities.Chart'
import { useAPI } from '../../Signum.React/Scripts/Hooks'
import * as Navigator from '../../Signum.React/Scripts/Navigator'
import * as Finder from '../../Signum.React/Scripts/Finder'
import { UserQueryEntity } from '../UserQueries/Signum.Entities.UserQueries'
import { getTypeInfos } from '@framework/Reflection'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

export interface TemplateControlsProps {
queryKey: string;
onInsert: (newCode: string) => void;
forHtml: boolean
forHtml: boolean;
widgetButtons?: boolean;
}

export default function TemplateControls(p: TemplateControlsProps) {

const [currentToken, setCurrentToken] = React.useState<QueryToken | undefined>(undefined)

const [currentToken, setCurrentToken] = React.useState<QueryToken | undefined>(undefined);
const qd = useAPI(() => Finder.getQueryDescription(p.queryKey), [p.queryKey]);

function renderButton(text: string, canClick: string | undefined, buildPattern: (key: string) => string) {
return <input type="button" disabled={!!canClick} className="btn btn-light btn-sm sf-button"
title={canClick} value={text}
onClick={() => p.onInsert(buildPattern(currentToken ? currentToken.fullKey : ""))} />;
onClick={() => ValueLineModal.show({
type: { name: "string" },
initialValue: buildPattern(currentToken ? currentToken.fullKey : ""),
title: "Template",
message: "Copy to clipboard: Ctrl+C, ESC",
initiallyFocused: true,
}).done()} />
}

function renderWidgetButton(text: React.ReactElement, getCode: () => Promise<string | undefined>) {
return <button className="btn btn-light btn-sm sf-button"

onClick={() =>
getCode()
.then(code =>
code &&
ValueLineModal.show({
type: { name: "string" },
valueLineType: "TextArea",
initialValue: code,
title: "Embedded Widget",
message: "Make a similar-looking Chart or Table in Excel and copy it to Word or PowerPoint. Then add the following code in the Alternative Text to bind the data:",
initiallyFocused: true,
})).done()} >{text}</button>
}




function canElement(): string | undefined {
let token = currentToken;

Expand Down Expand Up @@ -83,7 +117,7 @@ export default function TemplateControls(p: TemplateControlsProps) {
return null;

return (
<div>
<div className="d-flex">
<span className="rw-widget-sm">
<QueryTokenBuilder queryToken={ct} queryKey={p.queryKey} onTokenChange={t => setCurrentToken(t ?? undefined)} subTokenOptions={SubTokensOptions.CanAnyAll | SubTokensOptions.CanElement} readOnly={false} />
</span>
Expand All @@ -98,7 +132,40 @@ export default function TemplateControls(p: TemplateControlsProps) {
{renderButton("any", canElement(), token => p.forHtml ?
`<!--@any[${token}]--> <!--@notany--> <!--@endany-->` :
`@any[${token}] @notany @endany`)}




</div>
{p.widgetButtons &&
<div className="btn-group" style={{ marginLeft: "auto" }}>
{UserChartEntity.tryTypeInfo() && renderWidgetButton(<><FontAwesomeIcon icon={"chart-bar"} color={"darkviolet"} className="icon" /> {UserChartEntity.niceName()}</>, () => Finder.find<UserChartEntity>({
queryName: UserChartEntity,
filterOptions: [{
token: UserChartEntity.token(a => a.entity!.entityType!.entity!.cleanName),
operation: "IsIn",
value: [null, ...getTypeInfos(qd?.columns["Entity"].type!).map(a => a.name)]
}]
}).then(uc => uc && Navigator.API.fetch(uc).then(uce => {
var text = "UserChart:" + uce.guid;

if ((uce.chartScript.key.contains("Multi") || uce.chartScript.key.contains("Stacked")) && uce.columns[1].element.token != null /*Split*/)
text += "\nPivot(0, 1, 2)";

return text;
})))}
{
UserQueryEntity.tryTypeInfo() && renderWidgetButton(<><FontAwesomeIcon icon={["far", "list-alt"]} color={"dodgerblue"} className="icon" /> {UserQueryEntity.niceName()}</>, () => Finder.find<UserChartEntity>({
queryName: UserQueryEntity,
filterOptions: [{
token: UserQueryEntity.token(a => a.entity!.entityType!.entity!.cleanName),
operation: "IsIn",
value: [null, ...getTypeInfos(qd?.columns["Entity"].type!).map(a => a.name)]
}]
}).then(uc => uc && Navigator.API.fetch(uc).then(uce => "UserQuery:" + uce.guid)))
}
</div>
}
</div>
);
}
Expand Down
14 changes: 2 additions & 12 deletions Signum.React.Extensions/Word/Templates/WordTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ export default function WordTemplate(p: { ctx: TypeContext<WordTemplateEntity> }

const forceUpdate = useForceUpdate();

function handleOnInsert(newCode: string) {
ValueLineModal.show({
type: { name: "string" },
initialValue: newCode,
title: "Template",
message: "Copy to clipboard: Ctrl+C, ESC",
initiallyFocused: true,
}).done();
}

const ctx = p.ctx;
const ctx4 = p.ctx.subCtx({ labelColumns: 4 });
const canAggregate = ctx.value.groupResults ? SubTokensOptions.CanAggregate : 0;
Expand All @@ -55,8 +45,8 @@ export default function WordTemplate(p: { ctx: TypeContext<WordTemplateEntity> }
<Tab eventKey="template" title={ctx.niceName(a => a.template)}>
<ValueLine ctx={ctx.subCtx(f => f.fileName)} />
<div className="card form-xs" style={{ marginTop: "10px", marginBottom: "10px" }}>
<div className="card-header" style={{ padding: "5px" }}>
<TemplateControls queryKey={ctx.value.query.key} forHtml={false} onInsert={handleOnInsert} />
<div className="card-header" style={{ padding: "5px" }}>
<TemplateControls queryKey={ctx.value.query.key} forHtml={false} widgetButtons={true} />
</div>
</div>
<FileLine ctx={ctx.subCtx(e => e.template)} />
Expand Down

3 comments on commit e8d4ab2

@olmobrutall
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WordTemplate with embedded Excel charts from UserCharts / UserQueries

I've been doing some important improvements in Dashboards recently, but as a friend of mine put it

Dashboard are cool... but nobody uses them. Better an scheduled email with some embedded charts!

A little bit of history

A few years ago I implemented a project that required generating PowerPoint reports with embedded Excel charts filled with data from the database.

The implementation was made by generalizing WordTemplateEntity to also support PowerPoint files (naming sucks...) and writing the initial code to bind data from UserQueries or UserCharts into the data behind an embedded Excel chart.

The code worked for PowerPoint files (pptx) but was never implemented for Word documents (docx).

This week we had a similar problem but this time for docx files, so together with @thomasM1978 we finished this implementation and made some fixes to make it prime-time ready.

How it works

  1. In Excel, fill some dummy data and insert a new chart of the desired type, customize at will.
    image

  2. When ready, select the chart and copy it.

  3. Paste it in your Word / PowerPoint document, no special paste needed. Word will embedd the chart format and the data inside of the docx file itself. No reference to the original excel.

  4. Create a UserQuery or UserChart that is able to produce the data to fill the chart. If you are using a MultiLine/MultiColumns/StackedAreas/etc with an split column... don't worry! the tool is able to pivot the data to fit a usual tabular format with variable number of columns.
    image

  5. Word template UI has two new buttons to add a User Chart or User Query. Click the one for User Chart.
    image
    After selecting the user chart, it will show a modal window like this:
    image
    Copy the code.

  6. Paste the code in the Alternative Text of your chart, this is how it looks in Office 2013:
    image

  7. Save the docx and upload it to your WordTemplateEntity.

  8. In the WordTemplate, Create a new word report.
    image

This should be enough to make the WordTemplate detect the chart and bound the data.

You can fill the data of a chart using a UserQuery or a UserChart, doesn't really matter because only the query definition will be used.

The same workflow should work for PowerPoint too.

In PowerPoint you can bind excel tables from a UserQuery, but in Word get converted to word tables, so a @foreach should be used instead.

I've added a working example to Southwind Invoice WordTemplate.

Enjoy!

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on e8d4ab2 Nov 8, 2021 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rezanos
Copy link
Contributor

@rezanos rezanos commented on e8d4ab2 Nov 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

such a fantastic features these days! 👏👏👏
Thanks Olmo, Thanks @thomasM1978 🙏🙏
Long live Signum! 🎉🎉

Please sign in to comment.