unit Scaffolding.SourceGenerator;

interface

uses
  System.Classes, System.SysUtils,
  Generics.Collections,
  XData.Model.Classes,

  Scaffolding.Types;

type
  TScaffoldingSourceGenerator = class
  strict private
    FConfig: TScaffoldingConfig;
  private
    function GetSourceFileName(SourcePath, SourceFile: string): string;
    function GetDestinationFileName(SourcePath, SourceFile: string): string;

    function GetFieldType(AProp: TXDataProperty): string;
    function IsSupportedFieldType(AProp: TXDataProperty): boolean;
    function GetDescriptionProperty(AEntityType: TXDataEntityType): TXDataProperty;
    function HasDescriptionProperty(AEntityType: TXDataEntityType): Boolean;
    function HasEditView2(AEntityConfig: TScaffoldingEntityConfig): Boolean;

    procedure AddAfterCreateFormCallback(AEntityConfig: TScaffoldingEntityConfig; AfterCreateFormCallback: TStrings);

    procedure AddPasButtonsFields(AEntityConfig: TScaffoldingEntityConfig; AButtonsFields: TStrings);
    procedure AddPasButtonsClickDeclaration(AEntityConfig: TScaffoldingEntityConfig; ADeclaration: TStrings);
    procedure AddPasButtonsClickImplementation(AEntityConfig: TScaffoldingEntityConfig; AImplementation: TStrings);
    procedure AddDfmButtonsFields(AEntityConfig: TScaffoldingEntityConfig; AButtonsFields: TStrings);

    procedure AddPasDataset(ADatasetName: string; AEntityType: TXDataEntityType; ADataset: TStrings; AAddDescriptionField: Boolean);
    procedure AddPasDatasetField(ADatasetName: string; Prop: TXDataProperty; Fields: TStrings; AAddDescriptionField: Boolean);

    procedure AddDfmAssociationDataset(ADatasetName: string; AEntityType: TXDataEntityType; ADataset: TStrings; AAddDescriptionField: Boolean);
    procedure AddDfmDatasetField(ADatasetName: string; Prop: TXDataProperty; Fields: TStrings; AAddDescriptionField: Boolean);

    procedure AddPasAssociationDatasetEvents(ANavProp: TXDataNavigationProperty;
      ADeclaration: TStrings; AImplementation: TStrings; AIsPK: boolean);

    procedure AddPasLoadAssociationDataset(ANavProp: TXDataNavigationProperty; ALoadAssociationDataset: TStrings);
    procedure AddPasBindingComboboxToDataset(ANavProp: TXDataNavigationProperty; ABinding: TStrings);

    procedure AddPasInputControl(Prop: TXDataProperty; InputControls: TStrings);
    procedure AddDfmInputControl(Prop: TXDataProperty; InputControls: TStrings);
    procedure AddHtmlInputControl(Prop: TXDataProperty; InputControls: TStrings);
    procedure AddHtmlButtonSave(AEntityConfig: TScaffoldingEntityConfig; AButton: TStrings);
    procedure AddHtmlButtonDelete(AEntityConfig: TScaffoldingEntityConfig; AButton: TStrings);

    procedure CopyFile(SourcePath, SourceFile: string);

    procedure CopyTemplateFiles;
    procedure CopyCoreFiles;
    procedure CopyConfigFiles;

    procedure CopyConnectionModuleFiles;
    procedure CopyConnectionModulePasFile;
    procedure CopyConnectionModuleDfmFile;

    procedure CopyIndexHTML;
    procedure CopyViewCrudContainerFiles;
    procedure CopyViewErrorPageFiles;

    procedure CopyProjectFiles;
    procedure CopyDprFile;
    procedure CopyDprojFile;

    procedure CopyViewLoginFiles;

    procedure CopyViewMainFiles;
    procedure CopyViewMainPasFile;
    procedure CopyViewMainDfmFile;
    procedure CopyViewMainHtmlFile;

    procedure CopyViewHomeFiles;
    procedure CopyViewUserProfileFiles;

    procedure GenerateCrudFiles;
    procedure GenerateEntityCrudFiles(AEntityConfig: TScaffoldingEntityConfig);
    procedure GenerateEntityCrudListPasFile(AEntityConfig: TScaffoldingEntityConfig);
    procedure GenerateEntityCrudListDfmFile(AEntityConfig: TScaffoldingEntityConfig);
    procedure GenerateEntityCrudListHtmlFile(AEntityConfig: TScaffoldingEntityConfig);
    procedure GenerateEntityCrudEditPasFile(AEntityConfig: TScaffoldingEntityConfig);
    procedure GenerateEntityCrudEditDfmFile(AEntityConfig: TScaffoldingEntityConfig);
    procedure GenerateEntityCrudEditHtmlFile(AEntityConfig: TScaffoldingEntityConfig);
  public
    constructor Create(AConfig: TScaffoldingConfig);
    procedure Execute;
    function DprojFileName: string;
  end;

implementation

uses
  System.IOUtils,
  Aurelius.Global.Utils;

{ TScaffoldingSourceGenerator }

var
  CrudEditLabelTopPosition: Integer;
  CrudEditDatasetLeftPosition: Integer;
  CrudEditTabOrder: Integer;

procedure TScaffoldingSourceGenerator.AddAfterCreateFormCallback(
  AEntityConfig: TScaffoldingEntityConfig; AfterCreateFormCallback: TStrings);
begin
  AfterCreateFormCallback.Add('  procedure AfterCreate(AForm: TObject);');
  AfterCreateFormCallback.Add('  begin');
  AfterCreateFormCallback.Add('    TFView{ENTITY}Edit(AForm).FInserting := AInserting;');
  AfterCreateFormCallback.Add('    TFView{ENTITY}Edit(AForm).FId := AId;');
  AfterCreateFormCallback.Add('    TFView{ENTITY}Edit(AForm).FShowListProc := AShowListProc;');
  AfterCreateFormCallback.Add('    if AInserting then');
  AfterCreateFormCallback.Add('      TFView{ENTITY}Edit(AForm).InsertEntity');
  AfterCreateFormCallback.Add('    else');
  AfterCreateFormCallback.Add('      TFView{ENTITY}Edit(AForm).LoadEntity;');
  if AEntityConfig.CanDelete then
    AfterCreateFormCallback.Add('    TFView{ENTITY}Edit(AForm).btDelete.Enabled := not AInserting;');
  AfterCreateFormCallback.Add('  end;');
end;

procedure TScaffoldingSourceGenerator.AddDfmAssociationDataset(
  ADatasetName: string; AEntityType: TXDataEntityType;
  ADataset: TStrings; AAddDescriptionField: Boolean);
var
  Prop: TXDataProperty;
  NavProp: TXDataNavigationProperty;
begin
  Inc(CrudEditDatasetLeftPosition, 100);

  ADataset.Add(Format('  object %sDataset: TXDataWebDataSet', [ADatasetName]));
  ADataset.Add(Format('    AfterOpen = %sDatasetAfterOpen', [ADatasetName]));
  ADataset.Add(Format('    EntitySetName = ''%s''', [AEntityType.Name]));
  ADataset.Add(Format('    Connection = DMConnection.ApiConnection', []));
  ADataset.Add(Format('    Left = %d', [CrudEditDatasetLeftPosition]));
  ADataset.Add(Format('    Top = 8', []));

  for Prop in AEntityType.Key do
  begin
    if not IsSupportedFieldType(Prop) then
      Continue;

    AddDfmDatasetField(ADatasetName, Prop, ADataset, AAddDescriptionField);
  end;

  for Prop in AEntityType.Properties do
    if not AEntityType.Key.Contains(Prop) then
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;

      AddDfmDatasetField(ADatasetName, Prop, ADataset, AAddDescriptionField);
    end;

  for NavProp in AEntityType.NavigationProperties do
  begin
    // Only associations one-to-one
    if NavProp.Target.IsCollection then
      Continue;

    if not AEntityType.Key.Contains(NavProp) then
    begin
      AddDfmDatasetField(ADatasetName, NavProp, ADataset, AAddDescriptionField);
    end;
  end;

  ADataset.Add(Format('  end', []));
end;

procedure TScaffoldingSourceGenerator.AddDfmButtonsFields(
  AEntityConfig: TScaffoldingEntityConfig; AButtonsFields: TStrings);
begin
  if AEntityConfig.CanInsert or AEntityConfig.CanModify then
  begin
    Inc(CrudEditTabOrder);

    AButtonsFields.Add('  object btSave: TWebButton');
    AButtonsFields.Add('    Left = 24');
    AButtonsFields.Add('    Top = {EDITBUTTONTOPPOSITION}');
    AButtonsFields.Add('    Width = 96');
    AButtonsFields.Add('    Height = 25');
    AButtonsFields.Add('    Caption = ''Save''');
    AButtonsFields.Add('    ElementID = ''view.{ENTITY_LOWERCASE}.form.btSave''');
    AButtonsFields.Add(Format('    TabOrder = %d', [CrudEditTabOrder]));
    AButtonsFields.Add('    OnClick = btSaveClick');
    AButtonsFields.Add('  end');
  end;

  if AEntityConfig.CanDelete then
  begin
    Inc(CrudEditTabOrder);

    AButtonsFields.Add('  object btDelete: TWebButton');
    AButtonsFields.Add('    Left = 305');
    AButtonsFields.Add('    Top = {EDITBUTTONTOPPOSITION}');
    AButtonsFields.Add('    Width = 96');
    AButtonsFields.Add('    Height = 25');
    AButtonsFields.Add('    Caption = ''Delete''');
    AButtonsFields.Add('    ElementID = ''view.{ENTITY_LOWERCASE}.form.btDelete''');
    AButtonsFields.Add(Format('    TabOrder = %d', [CrudEditTabOrder]));
    AButtonsFields.Add('    OnClick = btDeleteClick');
    AButtonsFields.Add('  end');
  end;
end;

procedure TScaffoldingSourceGenerator.AddDfmDatasetField(ADatasetName: string;
  Prop: TXDataProperty; Fields: TStrings; AAddDescriptionField: Boolean);
var
  FieldName: string;
  DescriptionProp: TXDataProperty;
  TargetProp: TXDataProperty;
begin
  if Prop is TXDataNavigationProperty then
  begin
    Fields.Add(Format('    object %sDataset%s: %s', [ADatasetName, Prop.Name, 'TXDataWebEntityField']));
    Fields.Add(Format('      FieldName = ''%s''', [Prop.Name]));
    if Prop.Required then
      Fields.Add(Format('      Required = True', []))
    else
      Fields.Add(Format('      Required = False', []));
    Fields.Add(Format('    end', []));

    if not AAddDescriptionField then
      Exit;
    if not HasDescriptionProperty(TXDataNavigationProperty(Prop).Target) then
      Exit;
    DescriptionProp := GetDescriptionProperty(TXDataNavigationProperty(Prop).Target);
    TargetProp := DescriptionProp;
    FieldName := Format('%s.%s', [Prop.Name, DescriptionProp.Name]);
  end
  else
  begin
    TargetProp := Prop;
    FieldName := Format('%s', [Prop.Name]);
  end;

  Fields.Add(Format('    object %sDataset%s: %s', [ADatasetName, StringReplace(FieldName, '.', '', [rfReplaceAll]), GetFieldType(TargetProp)]));
  Fields.Add(Format('      FieldName = ''%s''', [FieldName]));
  if TargetProp.Required then
    Fields.Add(Format('      Required = True', []))
  else
    Fields.Add(Format('      Required = False', []));
  if TargetProp.IsSimple then
    if TXDataSimpleProperty(TargetProp).TypeKind = TXTypeKind.xtText then
      Fields.Add(Format('      Size = %d', [TXDataSimpleProperty(TargetProp).Length]));
  Fields.Add(Format('    end', []));
end;

procedure TScaffoldingSourceGenerator.AddDfmInputControl(Prop: TXDataProperty;
  InputControls: TStrings);
begin
  Inc(CrudEditLabelTopPosition, 27);
  Inc(CrudEditTabOrder);

  InputControls.Add(Format('  object lb%s: TWebLabel', [Prop.Name]));
  InputControls.Add(Format('    Left = 24', []));
  InputControls.Add(Format('    Top = %d', [CrudEditLabelTopPosition]));
  InputControls.Add(Format('    Width = 14', []));
  InputControls.Add(Format('    Height = 13', []));
  InputControls.Add(Format('    Caption = ''%s:''', [Prop.Name]));
  InputControls.Add(Format('    ElementID = ''view.{ENTITY_LOWERCASE}.form.lb%s''', [Prop.Name]));
  InputControls.Add(Format('    Transparent = True', []));
  InputControls.Add(Format('  end', []));

  if Prop is TXDataNavigationProperty then
  begin
    InputControls.Add(Format('  object cb%s: TWebComboBox', [Prop.Name]));
    InputControls.Add(Format('    Left = 221', []));
    InputControls.Add(Format('    Top = %d', [CrudEditLabelTopPosition - 3]));
    InputControls.Add(Format('    Width = 200', []));
    InputControls.Add(Format('    Height = 21', []));
    InputControls.Add(Format('    ElementID = ''view.{ENTITY_LOWERCASE}.form.cb%s''', [Prop.Name]));
    InputControls.Add(Format('    ItemIndex = -1', []));
    InputControls.Add(Format('    TabOrder = %d', [CrudEditTabOrder]));
    InputControls.Add(Format('    Text = ''cb%s''', [Prop.Name]));
    InputControls.Add(Format('  end', []));
  end
  else
  begin
    InputControls.Add(Format('  object ed%s: TWebDBEdit', [Prop.Name]));
    InputControls.Add(Format('    Left = 221', []));
    InputControls.Add(Format('    Top = %d', [CrudEditLabelTopPosition - 3]));
    InputControls.Add(Format('    Width = 200', []));
    InputControls.Add(Format('    Height = 21', []));
    InputControls.Add(Format('    ElementID = ''view.{ENTITY_LOWERCASE}.form.ed%s''', [Prop.Name]));
    InputControls.Add(Format('    Enabled = True', []));
    InputControls.Add(Format('    TabOrder = %d', [CrudEditTabOrder]));
    InputControls.Add(Format('    Text = ''ed%s''', [Prop.Name]));
    InputControls.Add(Format('    DataField = ''%s''', [Prop.Name]));
    InputControls.Add(Format('    DataSource = DataSource', []));
    InputControls.Add(Format('  end', []));
  end;
end;

procedure TScaffoldingSourceGenerator.AddHtmlButtonDelete(
  AEntityConfig: TScaffoldingEntityConfig; AButton: TStrings);
begin
  if AEntityConfig.CanDelete then
  begin
    AButton.Add('        <button id="view.{ENTITY_LOWERCASE}.form.btDelete" class="btn btn-danger pull-right">Delete</button>');
  end;
end;

procedure TScaffoldingSourceGenerator.AddHtmlButtonSave(
  AEntityConfig: TScaffoldingEntityConfig; AButton: TStrings);
begin
  if AEntityConfig.CanInsert or AEntityConfig.CanModify then
  begin
    AButton.Add('        <button id="view.{ENTITY_LOWERCASE}.form.btSave" class="btn btn-primary">Save</button>');
  end;
end;

procedure TScaffoldingSourceGenerator.AddHtmlInputControl(Prop: TXDataProperty;
  InputControls: TStrings);
begin
  InputControls.Add(Format(#9#9'<div class="form-group">', []));
  InputControls.Add(Format(#9#9#9'<label id="view.{ENTITY_LOWERCASE}.form.lb%s">%s</label>', [Prop.Name, Prop.Name]));

  if Prop is TXDataNavigationProperty then
    InputControls.Add(Format(#9#9#9'<select id="view.{ENTITY_LOWERCASE}.form.cb%s" class="form-control"></select>', [Prop.Name]))
  else
    InputControls.Add(Format(#9#9#9'<input id="view.{ENTITY_LOWERCASE}.form.ed%s" class="form-control">', [Prop.Name]));

  InputControls.Add(Format(#9#9'</div>', []));
end;

procedure TScaffoldingSourceGenerator.AddPasAssociationDatasetEvents(
  ANavProp: TXDataNavigationProperty; ADeclaration: TStrings;
  AImplementation: TStrings; AIsPK: boolean);
begin
  ADeclaration.Add(Format('    procedure %sDatasetAfterOpen(DataSet: TDataSet);', [ANavProp.Name]));

  AImplementation.Add(Format('procedure TFView{ENTITY}Edit.%sDatasetAfterOpen(DataSet: TDataSet);', [ANavProp.Name]));
  AImplementation.Add(Format('begin', []));
  AImplementation.Add(Format('  TCrudUtils.PopulateAssociationCombobox(cb%s,', [ANavProp.Name]));
  AImplementation.Add(Format('    TXDataWebDataSet(DataSet), ''%s'');', [GetDescriptionProperty(ANavProp.Target).Name]));
  AImplementation.Add(Format('', []));
  AImplementation.Add(Format('  if FInserting then', []));
  AImplementation.Add(Format('    cb%s.ItemIndex := -1', [ANavProp.Name]));
  AImplementation.Add(Format('  else', []));
  AImplementation.Add(Format('  begin', []));
  AImplementation.Add(Format('    TCrudUtils.SelectAssociationItem(cb%s, {ENTITY}Dataset%s);', [ANavProp.Name, ANavProp.Name]));
  if AIsPK then
    AImplementation.Add(Format('    cb%s.Enabled := False;', [ANavProp.Name]));
  AImplementation.Add(Format('  end;', []));
  AImplementation.Add(Format('end;', []));
  AImplementation.Add(Format('', []));
end;

procedure TScaffoldingSourceGenerator.AddPasBindingComboboxToDataset(
  ANavProp: TXDataNavigationProperty; ABinding: TStrings);
begin
  ABinding.Add(Format('    {ENTITY}Dataset%s.Value := cb%s.Items.Objects[cb%s.ItemIndex];', [ANavProp.Name, ANavProp.Name, ANavProp.Name]));
end;

procedure TScaffoldingSourceGenerator.AddPasButtonsClickDeclaration(
  AEntityConfig: TScaffoldingEntityConfig; ADeclaration: TStrings);
begin
  if AEntityConfig.CanInsert or AEntityConfig.CanModify then
    ADeclaration.Add('    procedure btSaveClick(Sender: TObject);');

  if AEntityConfig.CanDelete then
    ADeclaration.Add('    procedure btDeleteClick(Sender: TObject);');
end;

procedure TScaffoldingSourceGenerator.AddPasButtonsClickImplementation(
  AEntityConfig: TScaffoldingEntityConfig; AImplementation: TStrings);
begin
  if AEntityConfig.CanInsert or AEntityConfig.CanModify then
  begin
    AImplementation.Add('procedure TFView{ENTITY}Edit.btSaveClick(Sender: TObject);');
    AImplementation.Add('begin');
    AImplementation.Add('  if {ENTITY}Dataset.State in dsEditModes then');
    AImplementation.Add('  begin');
    AImplementation.Add('    {BINDINGCOMBOBOXTODATASET}    {ENTITY}Dataset.Post;');
    AImplementation.Add('    {ENTITY}Dataset.ApplyUpdates;');
    AImplementation.Add('  end');
    AImplementation.Add('  else');
    AImplementation.Add('    FShowListProc;');
    AImplementation.Add('end;');
    AImplementation.Add('');
  end;

  if AEntityConfig.CanDelete then
  begin
    AImplementation.Add('procedure TFView{ENTITY}Edit.btDeleteClick(Sender: TObject);');
    AImplementation.Add('begin');
    AImplementation.Add('  if {ENTITY}Dataset.State in dsEditModes then');
    AImplementation.Add('    {ENTITY}Dataset.Post;');
    AImplementation.Add('  XDataClient{ENTITY}Delete.Delete({ENTITY}Dataset.EntitySetName,');
    AImplementation.Add('    TJSObject({ENTITY}Dataset.CurrentData));');
    AImplementation.Add('end;');
    AImplementation.Add('');
  end;
end;

procedure TScaffoldingSourceGenerator.AddPasButtonsFields(
  AEntityConfig: TScaffoldingEntityConfig; AButtonsFields: TStrings);
begin
  if AEntityConfig.CanInsert or AEntityConfig.CanModify then
    AButtonsFields.Add('    btSave: TWebButton;');

  if AEntityConfig.CanDelete then
    AButtonsFields.Add('    btDelete: TWebButton;');
end;

procedure TScaffoldingSourceGenerator.AddPasDataset(ADatasetName: string;
  AEntityType: TXDataEntityType; ADataset: TStrings; AAddDescriptionField: Boolean);
var
  Prop: TXDataProperty;
  NavProp: TXDataNavigationProperty;
begin
  try
    ADataset.Add(Format('    %sDataset: TXDataWebDataSet;', [ADatasetName]));

    for Prop in AEntityType.Key do
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;
      AddPasDatasetField(ADatasetName, Prop, ADataset, AAddDescriptionField);
    end;

    for Prop in AEntityType.Properties do
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;
      if not Prop.IsSimple then
        Continue;

      if not AEntityType.Key.Contains(Prop) then
      begin
        AddPasDatasetField(ADatasetName, Prop, ADataset, AAddDescriptionField);
      end;
    end;

    for NavProp in AEntityType.NavigationProperties do
    begin
      // Only associations one-to-one
      if NavProp.Target.IsCollection then
        Continue;

      if not AEntityType.Key.Contains(NavProp) then
      begin
        AddPasDatasetField(ADatasetName, NavProp, ADataset, AAddDescriptionField);
      end;
    end;

  finally

  end;
end;

procedure TScaffoldingSourceGenerator.AddPasDatasetField(ADatasetName: string;
  Prop: TXDataProperty; Fields: TStrings; AAddDescriptionField: Boolean);
var
  DescriptionProp: TXDataProperty;
  TargetProp: TXDataProperty;
  FieldName: string;
begin
  if Prop is TXDataNavigationProperty then
  begin
    Fields.Add(Format('    %sDataset%s: %s;', [ADatasetName, Prop.Name, 'TXDataWebEntityField']));

    if not AAddDescriptionField then
      Exit;
    if not HasDescriptionProperty(TXDataNavigationProperty(Prop).Target) then
      Exit;
    DescriptionProp := GetDescriptionProperty(TXDataNavigationProperty(Prop).Target);
    TargetProp := DescriptionProp;
    FieldName := Format('%s%s', [Prop.Name, DescriptionProp.Name]);
  end
  else
  begin
    TargetProp := Prop;
    FieldName := Format('%s', [Prop.Name]);
  end;

  Fields.Add(Format('    %sDataset%s: %s;', [ADatasetName, FieldName, GetFieldType(TargetProp)]));
end;

procedure TScaffoldingSourceGenerator.AddPasInputControl(Prop: TXDataProperty;
  InputControls: TStrings);
begin
  InputControls.Add(Format('    lb%s: TWebLabel;', [Prop.Name]));
  if Prop is TXDataNavigationProperty then
    InputControls.Add(Format('    cb%s: TWebComboBox;', [Prop.Name]))
  else
    InputControls.Add(Format('    ed%s: TWebDBEdit;', [Prop.Name]));
end;

procedure TScaffoldingSourceGenerator.AddPasLoadAssociationDataset(
  ANavProp: TXDataNavigationProperty; ALoadAssociationDataset: TStrings);
begin
  ALoadAssociationDataset.Add(Format('  TCrudUtils.LoadAssociationDataset(%sDataset, ''%s'');', [ANavProp.Name, GetDescriptionProperty(ANavProp.Target).Name]));
end;

procedure TScaffoldingSourceGenerator.CopyConfigFiles;
var
  ConfigFile: TStrings;
begin
  ConfigFile := TStringList.Create;
  try
    ConfigFile.LoadFromFile(GetSourceFileName('config\', 'config.json'));
    ConfigFile.Text := StringReplace(ConfigFile.Text, '{AUTHURL}', FConfig.AuthUrl, [rfReplaceAll, rfIgnoreCase]);
    ConfigFile.Text := StringReplace(ConfigFile.Text, '{APIURL}', FConfig.ApiUrl, [rfReplaceAll, rfIgnoreCase]);

    if not TDirectory.Exists(ExtractFilePath(GetDestinationFileName('config\', 'config.json'))) then
      TDirectory.CreateDirectory(ExtractFilePath(GetDestinationFileName('config\', 'config.json')));

    ConfigFile.SaveToFile(GetDestinationFileName('config\', 'config.json'));
  finally
    ConfigFile.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyConnectionModuleDfmFile;
var
  DfmFile: TStrings;
  AuthObjects: TStrings;
begin
  DfmFile := TStringList.Create;
  AuthObjects := TStringList.Create;
  try
    DfmFile.LoadFromFile(GetSourceFileName('', 'ConnectionModule.dfm'));

    if FConfig.LoginEnabled then
    begin
      AuthObjects.Add(Format('  object AuthConnection: TXDataWebConnection', []));
      AuthObjects.Add(Format('    URL = ''%s''', [FConfig.AuthUrl]));
      AuthObjects.Add(Format('    OnError = AuthConnectionError', []));
      AuthObjects.Add(Format('    Left = 48', []));
      AuthObjects.Add(Format('    Top = 16', []));
      AuthObjects.Add(Format('  end', []));
    end;

    DfmFile.Text := StringReplace(DfmFile.Text, '{AUTHOBJECTS}', AuthObjects.Text, [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{APIURL}', FConfig.ApiUrl, [rfReplaceAll, rfIgnoreCase]);

    DfmFile.SaveToFile(GetDestinationFileName('', 'ConnectionModule.dfm'));
  finally
    DfmFile.Free;
    AuthObjects.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyConnectionModuleFiles;
begin
  CopyConnectionModulePasFile;
  CopyConnectionModuleDfmFile;
end;

procedure TScaffoldingSourceGenerator.CopyConnectionModulePasFile;
var
  PasFile: TStrings;
  AuthEventsDeclaration: TStrings;
  AuthEventsImplementation: TStrings;
  ConfigLoadedProc: TStrings;
begin
  PasFile := TStringList.Create;
  AuthEventsDeclaration := TStringList.Create;
  AuthEventsImplementation := TStringList.Create;
  ConfigLoadedProc := TStringList.Create;
  try
    PasFile.LoadFromFile(GetSourceFileName('', 'ConnectionModule.pas'));

    if FConfig.LoginEnabled then
    begin
      AuthEventsDeclaration.Add('    procedure AuthConnectionError(Error: TXDataWebConnectionError);');

      AuthEventsImplementation.Add(Format('procedure TDMConnection.AuthConnectionError(Error: TXDataWebConnectionError);', []));
      AuthEventsImplementation.Add(Format('begin', []));
      AuthEventsImplementation.Add(Format('  TFViewErrorPage.DisplayConnectionError(Error);', []));
      AuthEventsImplementation.Add(Format('end;', []));
    end;

    ConfigLoadedProc.Add(Format('  procedure ConfigLoaded(Config: TAppConfig);', []));
    ConfigLoadedProc.Add(Format('  begin', []));
    if FConfig.LoginEnabled then
    begin
      ConfigLoadedProc.Add(Format('    if Config.AuthUrl <> '''' then', []));
      ConfigLoadedProc.Add(Format('      AuthConnection.URL := Config.AuthUrl;', []));
      ConfigLoadedProc.Add(Format('', []));
    end;
    ConfigLoadedProc.Add(Format('    if Config.ApiUrl <> '''' then', []));
    ConfigLoadedProc.Add(Format('      ApiConnection.URL := Config.ApiUrl;', []));
    ConfigLoadedProc.Add(Format('', []));
    if FConfig.LoginEnabled then
      ConfigLoadedProc.Add(Format('    AuthConnection.Open(SuccessProc)', []))
    else
      ConfigLoadedProc.Add(Format('    ApiConnection.Open(SuccessProc);', []));
    ConfigLoadedProc.Add(Format('  end;', []));

    PasFile.Text := StringReplace(PasFile.Text, '{AUTHEVENTSDECLARATION}', AuthEventsDeclaration.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{AUTHEVENTSIMPLEMENTATION}', AuthEventsImplementation.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{CONFIGLOADEDPROC}', ConfigLoadedProc.Text, [rfReplaceAll, rfIgnoreCase]);

    PasFile.SaveToFile(GetDestinationFileName('', 'ConnectionModule.pas'));
  finally
    PasFile.Free;
    AuthEventsDeclaration.Free;
    AuthEventsImplementation.Free;
    ConfigLoadedProc.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyCoreFiles;
begin
  CopyFile('core\', 'App.Config.pas');
  CopyFile('core\', 'App.Types.pas');
  CopyFile('core\', 'Auth.Service.pas');
  CopyFile('core\', 'Dataset.Plugins.pas');
  CopyFile('core\', 'Grid.Plugins.pas');
  CopyFile('core\', 'Grid.Settings.pas');
  CopyFile('core\', 'Paginator.Plugins.pas');
  CopyFile('core\', 'Crud.Utils.pas');
end;

procedure TScaffoldingSourceGenerator.CopyDprFile;
var
  DprFile: TStrings;
  ViewUnits: TStrings;
  StartApplicationProc: TStrings;
  EntityConfig: TScaffoldingEntityConfig;
  Entity: string;
begin
  ViewUnits := TStringList.Create;
  StartApplicationProc := TStringList.Create;
  try
    for EntityConfig in FConfig.Entities do
    begin
      Entity := EntityConfig.EntitySet.Name;
      ViewUnits.Add(Format('  View.%s.List in ''View.%s.List.pas'' {FView%sList: TWebForm} {*.html},', [Entity, Entity, Entity]));
      if HasEditView2(EntityConfig) then
        ViewUnits.Add(Format('  View.%s.Edit in ''View.%s.Edit.pas'' {FView%sEdit: TWebForm} {*.html},', [Entity, Entity, Entity]));
    end;

    StartApplicationProc.Add(Format('procedure StartApplication;', [Entity]));
    StartApplicationProc.Add(Format('begin', [Entity]));

    if FConfig.LoginEnabled then
    begin
      StartApplicationProc.Add(Format('  if (not AuthService.Authenticated) or AuthService.TokenExpired then', [Entity]));
      StartApplicationProc.Add(Format('    DisplayLoginView', [Entity]));
      StartApplicationProc.Add(Format('  else', [Entity]));
      StartApplicationProc.Add(Format('    DisplayMainView;', [Entity]));
    end
    else
    begin
      StartApplicationProc.Add(Format('  DisplayMainView;', [Entity]));
    end;

    StartApplicationProc.Add(Format('end;', [Entity]));


    DprFile := TStringList.Create;
    try
      DprFile.LoadFromFile(GetSourceFileName('', 'SBAdminTemplate.dpr'));
      DprFile.Text := StringReplace(DprFile.Text, '{PROJECTNAME}', FConfig.ProjectName, [rfReplaceAll, rfIgnoreCase]);
      DprFile.Text := StringReplace(DprFile.Text, '{VIEWSUNITS}', ViewUnits.Text, [rfReplaceAll, rfIgnoreCase]);
      DprFile.Text := StringReplace(DprFile.Text, '{STARTAPPLICATIONPROC}', StartApplicationProc.Text, [rfReplaceAll, rfIgnoreCase]);
      DprFile.SaveToFile(GetDestinationFileName('', FConfig.ProjectName + '.dpr'));
    finally
      DprFile.Free;
    end;
  finally
    ViewUnits.Free;
    StartApplicationProc.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyDprojFile;
var
  DprojFile: TStrings;
  ViewUnits: TStrings;
  EntityConfig: TScaffoldingEntityConfig;
  Entity: string;
begin
  ViewUnits := TStringList.Create;
  try
    for EntityConfig in FConfig.Entities do
    begin
      Entity := EntityConfig.EntitySet.Name;
      ViewUnits.Add(Format(#9#9'<DCCReference Include="View.%s.List.pas">', [Entity]));
      ViewUnits.Add(Format(#9#9#9'<Form>FView%sList</Form>', [Entity]));
      ViewUnits.Add(Format(#9#9#9'<DesignClass>TWebForm</DesignClass>', []));
      ViewUnits.Add(Format(#9#9'</DCCReference>', []));

      if HasEditView2(EntityConfig) then
      begin
        ViewUnits.Add(Format(#9#9'<DCCReference Include="View.%s.Edit.pas">', [Entity]));
        ViewUnits.Add(Format(#9#9#9'<Form>FView%sEdit</Form>', [Entity]));
        ViewUnits.Add(Format(#9#9#9'<DesignClass>TWebForm</DesignClass>', []));
        ViewUnits.Add(Format(#9#9'</DCCReference>', []));
      end;
    end;

    DprojFile := TStringList.Create;
    try
      DprojFile.LoadFromFile(GetSourceFileName('', 'SBAdminTemplate.dproj'));
      DprojFile.Text := StringReplace(DprojFile.Text, '{PROJECTGUID}', TUtils.GuidToVariant(TUtils.NewGuid), [rfReplaceAll, rfIgnoreCase]);
      DprojFile.Text := StringReplace(DprojFile.Text, '{PROJECTNAME}', FConfig.ProjectName, [rfReplaceAll, rfIgnoreCase]);
      DprojFile.Text := StringReplace(DprojFile.Text, '<!-- {VIEWREFERENCES} -->', ViewUnits.Text, [rfReplaceAll, rfIgnoreCase]);
      DprojFile.SaveToFile(GetDestinationFileName('', FConfig.ProjectName + '.dproj'));
    finally
      DprojFile.Free;
    end;
  finally
    ViewUnits.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyIndexHTML;
begin
  CopyFile('', 'index.html');
end;

procedure TScaffoldingSourceGenerator.CopyProjectFiles;
begin
  CopyDprFile;
  CopyDprojFile;
end;

procedure TScaffoldingSourceGenerator.CopyFile(SourcePath,
  SourceFile: string);
var
  SourceFileName: string;
  DestinationPath: string;
  DestinationFileName: string;
begin
  SourceFileName := GetSourceFileName(SourcePath, SourceFile);

  if not TFile.Exists(SourceFileName) then
    raise EXDataScaffoldingException.CreateFmt('The template file was not found: %s', [SourceFileName]);

  DestinationFileName := GetDestinationFileName(SourcePath, SourceFile);
  DestinationPath := ExtractFilePath(DestinationFileName);

  if not TDirectory.Exists(DestinationPath) then
    TDirectory.CreateDirectory(DestinationPath);

  TFile.Copy(SourceFileName, DestinationFileName);
end;

procedure TScaffoldingSourceGenerator.CopyTemplateFiles;
begin
  // dist
  CopyFile('template\dist\css\', 'sb-admin-2.css');

  // vendor
  CopyFile('template\vendor\bootstrap\css\', 'bootstrap.min.css');
  CopyFile('template\vendor\bootstrap\js\', 'bootstrap.min.js');

  CopyFile('template\vendor\datatables-plugins\', 'dataTables.bootstrap.css');

  CopyFile('template\vendor\font-awesome\css\', 'font-awesome.min.css');
  CopyFile('template\vendor\font-awesome\fonts\', 'fontawesome-webfont.ttf');
  CopyFile('template\vendor\font-awesome\fonts\', 'fontawesome-webfont.woff');
  CopyFile('template\vendor\font-awesome\fonts\', 'fontawesome-webfont.woff2');

  CopyFile('template\vendor\jquery\', 'jquery.min.js');

  CopyFile('template\vendor\morrisjs\', 'morris.css');

  CopyFile('template\vendor\metisMenu\', 'metisMenu.min.css');
end;

procedure TScaffoldingSourceGenerator.CopyViewCrudContainerFiles;
begin
  CopyFile('', 'View.Crud.Container.pas');
  CopyFile('', 'View.Crud.Container.dfm');
  CopyFile('', 'View.Crud.Container.html');
end;

procedure TScaffoldingSourceGenerator.CopyViewErrorPageFiles;
begin
  CopyFile('', 'View.ErrorPage.pas');
  CopyFile('', 'View.ErrorPage.dfm');
  CopyFile('', 'View.ErrorPage.html');
end;

procedure TScaffoldingSourceGenerator.CopyViewHomeFiles;
begin
  CopyFile('', 'View.Home.pas');
  CopyFile('', 'View.Home.dfm');
  CopyFile('', 'View.Home.html');
end;

procedure TScaffoldingSourceGenerator.CopyViewLoginFiles;
begin
  CopyFile('', 'View.Login.pas');
  CopyFile('', 'View.Login.dfm');
  CopyFile('', 'View.Login.html');
end;

procedure TScaffoldingSourceGenerator.CopyViewMainDfmFile;
const
  TopGap = 19;
var
  EntityConfig: TScaffoldingEntityConfig;
  Entity: string;
  DfmFile: TStrings;
  AuthLabels: TStrings;
  LabelViews: TStrings;
  LabelTop: integer;
begin
  DfmFile := TStringList.Create;
  LabelViews := TStringList.Create;
  AuthLabels := TStringList.Create;
  try
    DfmFile.LoadFromFile(GetSourceFileName('', 'View.Main.dfm'));

    if FConfig.LoginEnabled then
    begin
      AuthLabels.Add(Format('  object lbLogout: TWebLinkLabel', []));
      AuthLabels.Add(Format('    Left = 551', []));
      AuthLabels.Add(Format('    Top = 46', []));
      AuthLabels.Add(Format('    Width = 33', []));
      AuthLabels.Add(Format('    Height = 13', []));
      AuthLabels.Add(Format('    ElementID = ''main.menu.logout''', []));
      AuthLabels.Add(Format('    OnClick = lbLogoutClick', []));
      AuthLabels.Add(Format('    Caption = ''Logout''', []));
      AuthLabels.Add(Format('  end', []));
      AuthLabels.Add(Format('  object lbUserProfile: TWebLinkLabel', []));
      AuthLabels.Add(Format('    Left = 529', []));
      AuthLabels.Add(Format('    Top = 27', []));
      AuthLabels.Add(Format('    Width = 55', []));
      AuthLabels.Add(Format('    Height = 13', []));
      AuthLabels.Add(Format('    ElementID = ''main.menu.userprofile''', []));
      AuthLabels.Add(Format('    OnClick = lbUserProfileClick', []));
      AuthLabels.Add(Format('    Caption = ''User Profile''', []));
      AuthLabels.Add(Format('  end', []));
      AuthLabels.Add(Format('  object lbUsername: TWebLabel', []));
      AuthLabels.Add(Format('    Left = 528', []));
      AuthLabels.Add(Format('    Top = 8', []));
      AuthLabels.Add(Format('    Width = 56', []));
      AuthLabels.Add(Format('    Height = 13', []));
      AuthLabels.Add(Format('    Caption = ''lbUsername''', []));
      AuthLabels.Add(Format('    ElementID = ''view.main.username''', []));
      AuthLabels.Add(Format('    Transparent = False', []));
      AuthLabels.Add(Format('  end', []));
    end;

    LabelTop := 111;
    for EntityConfig in FConfig.Entities do
    begin
      Entity := EntityConfig.EntitySet.Name;
      LabelViews.Add(Format('  object lbView%s: TWebLinkLabel', [Entity]));
      LabelViews.Add(Format('    Left = 24', []));
      LabelViews.Add(Format('    Top = %s', [IntToStr(LabelTop)]));
      LabelViews.Add(Format('    Width = 26', []));
      LabelViews.Add(Format('    Height = 13', []));
      LabelViews.Add(Format('    ElementID = ''main.menu.%s''', [LowerCase(Entity)]));
      LabelViews.Add(Format('    OnClick = lbView%sClick', [Entity]));
      LabelViews.Add(Format('    Caption = ''%s''', [Entity]));
      LabelViews.Add(Format('  end', []));
      Inc(LabelTop, TopGap);
    end;

    DfmFile.Text := StringReplace(DfmFile.Text, '{VIEWSLABELS}', LabelViews.Text, [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{AUTHLABELS}', AuthLabels.Text, [rfReplaceAll, rfIgnoreCase]);

    DfmFile.SaveToFile(GetDestinationFileName('', 'View.Main.dfm'));
  finally
    DfmFile.Free;
    LabelViews.Free;
    AuthLabels.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyViewMainFiles;
begin
  CopyViewMainPasFile;
  CopyViewMainDfmFile;
  CopyViewMainHtmlFile;
end;

procedure TScaffoldingSourceGenerator.CopyViewMainHtmlFile;
var
  EntityConfig: TScaffoldingEntityConfig;
  Entity: string;
  HtmlFile: TStrings;
  EntitiesMenu: TStrings;
  UserMenu: TStrings;
begin
  HtmlFile := TStringList.Create;
  EntitiesMenu := TStringList.Create;
  UserMenu := TStringList.Create;
  try
    HtmlFile.LoadFromFile(GetSourceFileName('', 'View.Main.html'));

    if FConfig.LoginEnabled then
    begin
      UserMenu.Add(Format(#9#9'<ul class="nav navbar-top-links navbar-right">', []));
      UserMenu.Add(Format(#9#9#9'<li class="dropdown">', []));
      UserMenu.Add(Format(#9#9#9#9'<a class="dropdown-toggle" data-toggle="dropdown" href="#" aria-expanded="false">', []));
      UserMenu.Add(Format(#9#9#9#9#9'<i class="fa fa-user fa-fw"></i> <span id="view.main.username"></span> <i class="fa fa-caret-down"></i>', []));
      UserMenu.Add(Format(#9#9#9#9'</a>', []));
      UserMenu.Add(Format(#9#9#9#9'<ul class="dropdown-menu dropdown-user">', []));
      UserMenu.Add(Format(#9#9#9#9#9'<li><a href="#" id="main.menu.userprofile"><i class="fa fa-user fa-fw"></i> User Profile</a>', []));
      UserMenu.Add(Format(#9#9#9#9#9'</li>', []));
      UserMenu.Add(Format(#9#9#9#9#9'<li class="divider"></li>', []));
      UserMenu.Add(Format(#9#9#9#9#9'<li><a href="#" id="main.menu.logout"><i class="fa fa-sign-out fa-fw"></i> Logout</a>', []));
      UserMenu.Add(Format(#9#9#9#9#9'</li>', []));
      UserMenu.Add(Format(#9#9#9#9'</ul>', []));
      UserMenu.Add(Format(#9#9#9#9'<!-- /.dropdown-user -->', []));
      UserMenu.Add(Format(#9#9#9'</li>', []));
      UserMenu.Add(Format(#9#9#9'<!-- /.dropdown -->', []));
      UserMenu.Add(Format(#9#9'</ul>', []));
    end;

    if FConfig.Entities.Count > 0 then
    begin
      EntitiesMenu.Add(Format(#9#9#9#9#9'<li>', []));
      EntitiesMenu.Add(Format(#9#9#9#9#9#9'<a><i class="fa fa-table fa-fw"></i> Entities</a>', []));
      EntitiesMenu.Add(Format(#9#9#9#9#9#9'<ul class="nav nav-second-level">', []));
    end;

    for EntityConfig in FConfig.Entities do
    begin
      Entity := EntityConfig.EntitySet.Name;
      EntitiesMenu.Add(Format(#9#9#9#9#9#9#9'<li>', []));
      EntitiesMenu.Add(Format(#9#9#9#9#9#9#9#9'<a href="#" id="main.menu.%s">%s</a>', [LowerCase(Entity), Entity]));
      EntitiesMenu.Add(Format(#9#9#9#9#9#9#9'</li>', []));
    end;

    if FConfig.Entities.Count > 0 then
    begin
      EntitiesMenu.Add(Format(#9#9#9#9#9#9'</ul>', []));
      EntitiesMenu.Add(Format(#9#9#9#9#9'</li>', []));
    end;

    HtmlFile.Text := StringReplace(HtmlFile.Text, '<!-- {USERMENU} -->', UserMenu.Text, [rfReplaceAll, rfIgnoreCase]);
    HtmlFile.Text := StringReplace(HtmlFile.Text, '<!-- {ENTITIESMENU} -->', EntitiesMenu.Text, [rfReplaceAll, rfIgnoreCase]);

    HtmlFile.SaveToFile(GetDestinationFileName('', 'View.Main.html'));
  finally
    HtmlFile.Free;
    EntitiesMenu.Free;
    UserMenu.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyViewMainPasFile;
var
  EntityConfig: TScaffoldingEntityConfig;
  Entity: string;
  PasFile: TStrings;
  AuthLabels: TStrings;
  AuthLabelsClickDeclaration: TStrings;
  AuthLabelsClickImplementation: TStrings;
  AuthFields: TStrings;
  LabelViews: TStrings;
  LabelViewsClickDeclaration: TStrings;
  ViewsUnits: TStrings;
  LabelViewsClickImplementation: TStrings;
  FormCreateProc: TStrings;
begin
  PasFile := TStringList.Create;
  AuthLabels := TStringList.Create;
  AuthLabelsClickDeclaration := TStringList.Create;
  AuthLabelsClickImplementation := TStringList.Create;
  AuthFields := TStringList.Create;
  LabelViews := TStringList.Create;
  LabelViewsClickDeclaration := TStringList.Create;
  ViewsUnits := TStringList.Create;
  LabelViewsClickImplementation := TStringList.Create;
  FormCreateProc := TStringList.Create;
  try
    PasFile.LoadFromFile(GetSourceFileName('', 'View.Main.pas'));

    if FConfig.LoginEnabled then
    begin
      AuthLabels.Add(Format('    lbLogout: TWebLinkLabel;', []));
      AuthLabels.Add(Format('    lbUserProfile: TWebLinkLabel;', []));
      AuthLabels.Add(Format('    lbUsername: TWebLabel;', []));

      AuthFields.Add(Format('    FLogoutProc: TLogoutProc;', []));

      AuthLabelsClickDeclaration.Add(Format('    procedure lbLogoutClick(Sender: TObject);', []));
      AuthLabelsClickDeclaration.Add(Format('    procedure lbUserProfileClick(Sender: TObject);', []));

      AuthLabelsClickImplementation.Add(Format('procedure TFViewMain.lbLogoutClick(Sender: TObject);', []));
      AuthLabelsClickImplementation.Add(Format('begin', []));
      AuthLabelsClickImplementation.Add(Format('  // logout', []));
      AuthLabelsClickImplementation.Add(Format('  FLogoutProc;', []));
      AuthLabelsClickImplementation.Add(Format('end;', []));
      AuthLabelsClickImplementation.Add(Format('', []));
      AuthLabelsClickImplementation.Add(Format('procedure TFViewMain.lbUserProfileClick(Sender: TObject);', []));
      AuthLabelsClickImplementation.Add(Format('begin', []));
      AuthLabelsClickImplementation.Add(Format('  ShowForm(TFViewUserProfile);', []));
      AuthLabelsClickImplementation.Add(Format('end;', []));
    end;

    FormCreateProc.Add(Format('procedure TFViewMain.WebFormCreate(Sender: TObject);', []));
    FormCreateProc.Add(Format('begin', []));
    if FConfig.LoginEnabled then
      FormCreateProc.Add(Format('  lbUsername.Caption := JS.toString(AuthService.TokenPayload.Properties[''user'']);', []));
    FormCreateProc.Add(Format('  FChildForm := nil;', []));
    FormCreateProc.Add(Format('  ShowForm(TFViewHome);', []));
    FormCreateProc.Add(Format('end;', []));


    for EntityConfig in FConfig.Entities do
    begin
      Entity := EntityConfig.EntitySet.Name;

      LabelViews.Add(Format('    lbView%s: TWebLinkLabel;', [Entity]));

      LabelViewsClickDeclaration.Add(Format('    procedure lbView%sClick(Sender: TObject);', [Entity]));

      ViewsUnits.Add(Format('  View.%s.List,', [Entity]));
      if HasEditView2(EntityConfig) then
        ViewsUnits.Add(Format('  View.%s.Edit,', [Entity]));

      LabelViewsClickImplementation.Add(Format('procedure TFViewMain.lbView%sClick(Sender: TObject);', [Entity]));
      LabelViewsClickImplementation.Add(Format('', []));
      LabelViewsClickImplementation.Add(Format('  function Create%sListForm(const AElementId: string; AEditProc: TEditProc): TWebForm;', [Entity]));
      LabelViewsClickImplementation.Add(Format('  begin', []));

      if HasEditView2(EntityConfig) then
        LabelViewsClickImplementation.Add(Format('    Result := TFView%sList.CreateForm(AElementId, AEditProc);', [Entity]))
      else
        LabelViewsClickImplementation.Add(Format('    Result := TFView%sList.CreateForm(AElementId);', [Entity]));

      LabelViewsClickImplementation.Add(Format('  end;', []));
      LabelViewsClickImplementation.Add(Format('', []));

      if HasEditView2(EntityConfig) then
      begin
        LabelViewsClickImplementation.Add(Format('  function Create%sEditForm(const AElementId: string; AListProc: TListProc;', [Entity]));
        LabelViewsClickImplementation.Add(Format('    AId: JSValue): TWebForm;', []));
        LabelViewsClickImplementation.Add(Format('  begin', []));
        LabelViewsClickImplementation.Add(Format('    Result := TFView%sEdit.CreateForm(AElementId, AListProc, AId);', [Entity]));
        LabelViewsClickImplementation.Add(Format('  end;', []));
        LabelViewsClickImplementation.Add(Format('', []));
      end;

      LabelViewsClickImplementation.Add(Format('begin', []));
      if HasEditView2(EntityConfig) then
        LabelViewsClickImplementation.Add(Format('  ShowCrudContainer(TFView%sList, ''%s'', @Create%sListForm, @Create%sEditForm);', [Entity, Entity, Entity, Entity]))
      else
        LabelViewsClickImplementation.Add(Format('  ShowCrudContainer(TFView%sList, ''%s'', @Create%sListForm, nil);', [Entity, Entity, Entity]));
      LabelViewsClickImplementation.Add(Format('end;', []));
      LabelViewsClickImplementation.Add(Format('', []));
    end;

    PasFile.Text := StringReplace(PasFile.Text, '{AUTHLABELS}', AuthLabels.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{AUTHLABELSCLICKDECLARATION}', AuthLabelsClickDeclaration.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{AUTHLABELSCLICKIMPLEMENTATION}', AuthLabelsClickImplementation.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{AUTHFIELDS}', AuthFields.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{LABELVIEWS}', LabelViews.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{LABELVIEWSCLICKDECLARATION}', LabelViewsClickDeclaration.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{VIEWSUNITS}', ViewsUnits.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{LABELVIEWSCLICKIMPLEMENTATION}', LabelViewsClickImplementation.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{FORMCREATEPROC}', FormCreateProc.Text, [rfReplaceAll, rfIgnoreCase]);

    PasFile.SaveToFile(GetDestinationFileName('', 'View.Main.pas'));
  finally
    PasFile.Free;
    AuthLabels.Free;
    AuthFields.Free;
    LabelViews.Free;
    LabelViewsClickDeclaration.Free;
    ViewsUnits.Free;
    LabelViewsClickImplementation.Free;
    AuthLabelsClickDeclaration.Free;
    AuthLabelsClickImplementation.Free;
    FormCreateProc.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.CopyViewUserProfileFiles;
begin
  CopyFile('', 'View.UserProfile.pas');
  CopyFile('', 'View.UserProfile.dfm');
  CopyFile('', 'View.UserProfile.html');
end;

constructor TScaffoldingSourceGenerator.Create(AConfig: TScaffoldingConfig);
begin
  FConfig := AConfig;
end;

function TScaffoldingSourceGenerator.DprojFileName: string;
begin
  Result := GetDestinationFileName('', FConfig.ProjectName + '.dproj');
end;

procedure TScaffoldingSourceGenerator.Execute;
begin
  CopyProjectFiles;
  CopyTemplateFiles;
  CopyCoreFiles;
  CopyConfigFiles;
  CopyConnectionModuleFiles;
  CopyIndexHTML;
  CopyViewCrudContainerFiles;
  CopyViewErrorPageFiles;
  CopyViewLoginFiles;
  CopyViewMainFiles;
  CopyViewHomeFiles;
  CopyViewUserProfileFiles;

  GenerateCrudFiles;
end;

procedure TScaffoldingSourceGenerator.GenerateCrudFiles;
var
  EntityConfig: TScaffoldingEntityConfig;
begin
  for EntityConfig in FConfig.Entities do
    GenerateEntityCrudFiles(EntityConfig);
end;

procedure TScaffoldingSourceGenerator.GenerateEntityCrudEditDfmFile(
  AEntityConfig: TScaffoldingEntityConfig);
var
  Entity: string;
  DfmFile: TStrings;
  ButtonsFields: TStrings;
  InputControls: TStrings;
  DatasetFields: TStrings;
  AssociationsDatasets: TStrings;
  Prop: TXDataProperty;
  NavProp: TXDataNavigationProperty;
begin
  DfmFile := TStringList.Create;
  ButtonsFields := TStringList.Create;
  InputControls := TStringList.Create;
  DatasetFields := TStringList.Create;
  AssociationsDatasets := TStringList.Create;
  try
    Entity := AEntityConfig.EntitySet.Name;
    DfmFile.LoadFromFile(GetSourceFileName('', 'View.Crud.Edit.dfm'));

    CrudEditTabOrder := 3;
    CrudEditLabelTopPosition := 165;
    CrudEditDatasetLeftPosition := 184;

    AddDfmButtonsFields(AEntityConfig, ButtonsFields);

    for Prop in AEntityConfig.EntitySet.EntityType.Key do
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;

      AddDfmDatasetField(Entity, Prop, DatasetFields, False);
      AddDfmInputControl(Prop, InputControls);

      if Prop is TXDataNavigationProperty then
      begin
        NavProp := TXDataNavigationProperty(Prop);
        AddDfmAssociationDataset(NavProp.Name, NavProp.Target, AssociationsDatasets, False);
      end;
    end;

    for Prop in AEntityConfig.EntitySet.EntityType.Properties do
      if not AEntityConfig.EntitySet.EntityType.Key.Contains(Prop) then
      begin
        if not IsSupportedFieldType(Prop) then
          Continue;
        if not Prop.IsSimple then
          Continue;

        AddDfmDatasetField(Entity, Prop, DatasetFields, False);
        AddDfmInputControl(Prop, InputControls);
      end;

    for NavProp in AEntityConfig.EntitySet.EntityType.NavigationProperties do
    begin
      // Only associations one-to-one
      if NavProp.Target.IsCollection then
        Continue;

      if not AEntityConfig.EntitySet.EntityType.Key.Contains(NavProp) then
      begin
        AddDfmDatasetField(Entity, NavProp, DatasetFields, False);
        AddDfmInputControl(NavProp, InputControls);
        AddDfmAssociationDataset(NavProp.Name, NavProp.Target, AssociationsDatasets, False);
      end;
    end;

    DfmFile.Text := StringReplace(DfmFile.Text, '{BUTTONSFIELDS}', ButtonsFields.Text, [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{INPUTCONTROLS}', InputControls.Text, [rfReplaceAll, rfIgnoreCase]);

    DfmFile.Text := StringReplace(DfmFile.Text, '{FORMHEIGHT}', IntToStr(CrudEditLabelTopPosition + 100), [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{EDITBUTTONTOPPOSITION}', IntToStr(CrudEditLabelTopPosition + 40), [rfReplaceAll, rfIgnoreCase]);

    DfmFile.Text := StringReplace(DfmFile.Text, '{DATASETFIELDS}', DatasetFields.Text, [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{ASSOCIATIONSDATASETS}', AssociationsDatasets.Text, [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{ENTITY}', Entity, [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{ENTITY_LOWERCASE}', LowerCase(Entity), [rfReplaceAll, rfIgnoreCase]);

    DfmFile.SaveToFile(GetDestinationFileName('', Format('View.%s.Edit.dfm', [Entity])));
  finally
    DfmFile.Free;
    ButtonsFields.Free;
    InputControls.Free;
    DatasetFields.Free;
    AssociationsDatasets.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.GenerateEntityCrudEditHtmlFile(
  AEntityConfig: TScaffoldingEntityConfig);
var
  Entity: string;
  HtmlFile: TStrings;
  InputControls: TStrings;
  ButtonSave: TStrings;
  ButtonDelete: TStrings;
  Prop: TXDataProperty;
  NavProp: TXDataNavigationProperty;
begin
  HtmlFile := TStringList.Create;
  InputControls := TStringList.Create;
  ButtonSave := TStringList.Create;
  ButtonDelete := TStringList.Create;
  try
    Entity := AEntityConfig.EntitySet.Name;
    HtmlFile.LoadFromFile(GetSourceFileName('', 'View.Crud.Edit.html'));

    AddHtmlButtonSave(AEntityConfig, ButtonSave);
    AddHtmlButtonDelete(AEntityConfig, ButtonDelete);

    for Prop in AEntityConfig.EntitySet.EntityType.Key do
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;

      AddHtmlInputControl(Prop, InputControls);
    end;

    for Prop in AEntityConfig.EntitySet.EntityType.Properties do
      if not AEntityConfig.EntitySet.EntityType.Key.Contains(Prop) then
      begin
        if not IsSupportedFieldType(Prop) then
          Continue;
        if not Prop.IsSimple then
          Continue;

        AddHtmlInputControl(Prop, InputControls);
      end;

    for NavProp in AEntityConfig.EntitySet.EntityType.NavigationProperties do
    begin
      // Only associations one-to-one
      if NavProp.Target.IsCollection then
        Continue;

      if not AEntityConfig.EntitySet.EntityType.Key.Contains(NavProp) then
      begin
        AddHtmlInputControl(NavProp, InputControls);
      end;
    end;

    HtmlFile.Text := StringReplace(HtmlFile.Text, '<!-- {INPUTCONTROLS} -->', InputControls.Text, [rfReplaceAll, rfIgnoreCase]);
    HtmlFile.Text := StringReplace(HtmlFile.Text, '<!-- {BUTTONSAVE} -->', ButtonSave.Text, [rfReplaceAll, rfIgnoreCase]);
    HtmlFile.Text := StringReplace(HtmlFile.Text, '<!-- {BUTTONDELETE} -->', ButtonDelete.Text, [rfReplaceAll, rfIgnoreCase]);
    HtmlFile.Text := StringReplace(HtmlFile.Text, '{ENTITY}', Entity, [rfReplaceAll, rfIgnoreCase]);
    HtmlFile.Text := StringReplace(HtmlFile.Text, '{ENTITY_LOWERCASE}', LowerCase(Entity), [rfReplaceAll, rfIgnoreCase]);

    HtmlFile.SaveToFile(GetDestinationFileName('', Format('View.%s.Edit.html', [Entity])));
  finally
    HtmlFile.Free;
    InputControls.Free;
    ButtonSave.Free;
    ButtonDelete.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.GenerateEntityCrudEditPasFile(
  AEntityConfig: TScaffoldingEntityConfig);
var
  Entity: string;
  PasFile: TStrings;
  AfterCreateFormCallback: TStrings;
  ButtonsFields: TStrings;
  ButtonsClickDeclaration: TStrings;
  ButtonsClickImplementation: TStrings;
  DatasetFields: TStrings;
  AssociationsDatasetsEventsDeclaration: TStrings;
  AssociationsDatasetsEventsImplementation: TStrings;
  InputControls: TStrings;
  LoadAssociationsDatasets: TStrings;
  BindingComboboxToDataset: TStrings;
  Prop: TXDataProperty;
  NavProp: TXDataNavigationProperty;
begin
  PasFile := TStringList.Create;
  AfterCreateFormCallback := TStringList.Create;
  ButtonsFields := TStringList.Create;
  ButtonsClickDeclaration := TStringList.Create;
  ButtonsClickImplementation := TStringList.Create;
  DatasetFields := TStringList.Create;
  InputControls := TStringList.Create;
  AssociationsDatasetsEventsDeclaration := TStringList.Create;
  AssociationsDatasetsEventsImplementation := TStringList.Create;
  LoadAssociationsDatasets := TStringList.Create;
  BindingComboboxToDataset := TStringList.Create;
  try
    Entity := AEntityConfig.EntitySet.Name;
    PasFile.LoadFromFile(GetSourceFileName('', 'View.Crud.Edit.pas'));    

    AddAfterCreateFormCallback(AEntityConfig, AfterCreateFormCallback);

    AddPasButtonsFields(AEntityConfig, ButtonsFields);
    AddPasButtonsClickDeclaration(AEntityConfig, ButtonsClickDeclaration);
    AddPasButtonsClickImplementation(AEntityConfig, ButtonsClickImplementation);
    AddPasDataset(AEntityConfig.EntitySet.EntityType.Name, AEntityConfig.EntitySet.EntityType, DatasetFields, False);

    for Prop in AEntityConfig.EntitySet.EntityType.Key do
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;
      AddPasInputControl(Prop, InputControls);

      if Prop is TXDataNavigationProperty then
      begin
        NavProp := TXDataNavigationProperty(Prop);
        AddPasDataset(NavProp.Name, NavProp.Target, DatasetFields, False);
        AddPasAssociationDatasetEvents(NavProp, AssociationsDatasetsEventsDeclaration, AssociationsDatasetsEventsImplementation, True);
        AddPasLoadAssociationDataset(NavProp, LoadAssociationsDatasets);
        AddPasBindingComboboxToDataset(NavProp, BindingComboboxToDataset);
      end;
    end;

    for Prop in AEntityConfig.EntitySet.EntityType.Properties do
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;
      if not Prop.IsSimple then
        Continue;

      if not AEntityConfig.EntitySet.EntityType.Key.Contains(Prop) then
      begin
        AddPasInputControl(Prop, InputControls);
      end;
    end;

    for NavProp in AEntityConfig.EntitySet.EntityType.NavigationProperties do
    begin
      // Only associations one-to-one
      if NavProp.Target.IsCollection then
        Continue;

      if not AEntityConfig.EntitySet.EntityType.Key.Contains(NavProp) then
      begin
        AddPasDataset(NavProp.Name, NavProp.Target, DatasetFields, False);
        AddPasAssociationDatasetEvents(NavProp, AssociationsDatasetsEventsDeclaration, AssociationsDatasetsEventsImplementation, False);
        AddPasLoadAssociationDataset(NavProp, LoadAssociationsDatasets);
        AddPasBindingComboboxToDataset(NavProp, BindingComboboxToDataset);
        AddPasInputControl(NavProp, InputControls);
      end;
    end;

    PasFile.Text := StringReplace(PasFile.Text, '{AFTERCREATEFORMCALLBACK}', AfterCreateFormCallback.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{INPUTCONTROLS}', InputControls.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{BUTTONSFIELDS}', ButtonsFields.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{BUTTONSCLICKDECLARATION}', ButtonsClickDeclaration.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{BUTTONSCLICKIMPLEMENTATION}', ButtonsClickImplementation.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{BUTTONSFIELDS}', ButtonsFields.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{DATASETFIELDS}', DatasetFields.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{ASSOCIATIONSDATASETSEVENTSDECLARATION}', AssociationsDatasetsEventsDeclaration.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{ASSOCIATIONSDATASETSEVENTSIMPLEMENTATION}', AssociationsDatasetsEventsImplementation.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{LOADASSOCIATIONSDATASETS}', LoadAssociationsDatasets.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{BINDINGCOMBOBOXTODATASET}', BindingComboboxToDataset.Text, [rfReplaceAll, rfIgnoreCase]);
    PasFile.Text := StringReplace(PasFile.Text, '{ENTITY}', Entity, [rfReplaceAll, rfIgnoreCase]);

    PasFile.SaveToFile(GetDestinationFileName('', Format('View.%s.Edit.pas', [Entity])));
  finally
    PasFile.Free;
    AfterCreateFormCallback.Free;
    ButtonsFields.Free;
    ButtonsClickDeclaration.Free;
    ButtonsClickImplementation.Free;
    DatasetFields.Free;
    InputControls.Free;
    AssociationsDatasetsEventsDeclaration.Free;
    AssociationsDatasetsEventsImplementation.Free;
    LoadAssociationsDatasets.Free;
    BindingComboboxToDataset.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.GenerateEntityCrudFiles(AEntityConfig: TScaffoldingEntityConfig);
begin
  GenerateEntityCrudListPasFile(AEntityConfig);
  GenerateEntityCrudListDfmFile(AEntityConfig);
  GenerateEntityCrudListHtmlFile(AEntityConfig);

  if HasEditView2(AEntityConfig) then
  begin
    GenerateEntityCrudEditPasFile(AEntityConfig);
    GenerateEntityCrudEditDfmFile(AEntityConfig);
    GenerateEntityCrudEditHtmlFile(AEntityConfig);  
  end;  
end;

procedure TScaffoldingSourceGenerator.GenerateEntityCrudListDfmFile(
  AEntityConfig: TScaffoldingEntityConfig);

  procedure AddGridColumn(Prop: TXDataProperty; Columns: TStrings); overload;
  var
    FieldName: string;
    DescriptionProp: TXDataProperty;
  begin
    if Prop is TXDataNavigationProperty then
    begin
      DescriptionProp := GetDescriptionProperty(TXDataNavigationProperty(Prop).Target);
      FieldName := Format('%s.%s', [Prop.Name, DescriptionProp.Name]);
    end
    else
    begin
      FieldName := Format('%s', [Prop.Name]);
    end;

    Columns.Add(Format('      item', []));
    Columns.Add(Format('        DataField = ''%s''', [FieldName]));
    Columns.Add(Format('        Title = ''%s''', [FieldName]));
    Columns.Add(Format('      end', []));
  end;

  procedure AddButtonNew(Button: TStrings);
  begin
    Button.Add('  object btNew: TWebButton');
    Button.Add('    Left = 8');
    Button.Add('    Top = 8');
    Button.Add('    Width = 50');
    Button.Add('    Height = 25');
    Button.Add('    Caption = ''New''');
    Button.Add('    ElementID = ''view.{ENTITY_LOWERCASE}.list.newbutton''');
    Button.Add('    TabOrder = 0');
    Button.Add('    OnClick = btNewClick');
    Button.Add('  end');
  end;

  procedure ReplaceTableControlOnGetCellChildrenEvent(Source: TStrings);
  begin
    if AEntityConfig.CanModify or AEntityConfig.CanDelete then
      Source.Text := StringReplace(Source.Text, '{TABLECONTROLONGETCELLCHILDRENEVENT}', '    OnGetCellChildren = WebDBTableControl1GetCellChildren'#13#10, [rfReplaceAll, rfIgnoreCase])
    else
      Source.Text := StringReplace(Source.Text, '{TABLECONTROLONGETCELLCHILDRENEVENT}', '', [rfReplaceAll, rfIgnoreCase]);
  end;

var
  Entity: string;
  DfmFile: TStrings;
  ButtonNew: TStrings;
  GridColumns: TStrings;
  DatasetFields: TStrings;
  Prop: TXDataProperty;
  NavProp: TXDataNavigationProperty;
begin
  DfmFile := TStringList.Create;
  ButtonNew := TStringList.Create;
  GridColumns := TStringList.Create;
  DatasetFields := TStringList.Create;
  try
    Entity := AEntityConfig.EntitySet.Name;
    DfmFile.LoadFromFile(GetSourceFileName('', 'View.Crud.List.dfm'));

    if AEntityConfig.CanInsert then
      AddButtonNew(ButtonNew);

    ReplaceTableControlOnGetCellChildrenEvent(DfmFile);

    GridColumns.Add('    Columns = <');
    for Prop in AEntityConfig.EntitySet.EntityType.Key do
    begin
      if not IsSupportedFieldType(Prop) then
        Continue;

      AddGridColumn(Prop, GridColumns);
      AddDfmDatasetField(Entity, Prop, DatasetFields, True);
    end;

    for Prop in AEntityConfig.EntitySet.EntityType.Properties do
      if not AEntityConfig.EntitySet.EntityType.Key.Contains(Prop) then
      begin
        if not IsSupportedFieldType(Prop) then
          Continue;

        AddGridColumn(Prop, GridColumns);
        AddDfmDatasetField(Entity, Prop, DatasetFields, True);
      end;

    for NavProp in AEntityConfig.EntitySet.EntityType.NavigationProperties do
    begin
      // Only associations one-to-one
      if NavProp.Target.IsCollection then
        Continue;

      if not AEntityConfig.EntitySet.EntityType.Key.Contains(NavProp) then
      begin
        AddGridColumn(NavProp, GridColumns);
        AddDfmDatasetField(Entity, NavProp, DatasetFields, True);
      end;
    end;

    if AEntityConfig.CanModify or AEntityConfig.CanDelete then
    begin
      GridColumns.Add(Format('      item', []));
      GridColumns.Add(Format('        Title = ''Actions''', []));
      GridColumns.Add(Format('      end', []));
    end;

    DfmFile.Text := StringReplace(DfmFile.Text, '{BUTTONNEWOBJ}', ButtonNew.Text, [rfReplaceAll, rfIgnoreCase]);

    GridColumns[GridColumns.Count - 1] := GridColumns[GridColumns.Count - 1] + '>';
    DfmFile.Text := StringReplace(DfmFile.Text, '{GRIDCOLUMNS}', GridColumns.Text, [rfReplaceAll, rfIgnoreCase]);

    DfmFile.Text := StringReplace(DfmFile.Text, '{DATASETFIELDS}', DatasetFields.Text, [rfReplaceAll, rfIgnoreCase]);

    DfmFile.Text := StringReplace(DfmFile.Text, '{ENTITY}', Entity, [rfReplaceAll, rfIgnoreCase]);
    DfmFile.Text := StringReplace(DfmFile.Text, '{ENTITY_LOWERCASE}', LowerCase(Entity), [rfReplaceAll, rfIgnoreCase]);

    DfmFile.SaveToFile(GetDestinationFileName('', Format('View.%s.List.dfm', [Entity])));
  finally
    DfmFile.Free;
    ButtonNew.Free;
    GridColumns.Free;
    DatasetFields.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.GenerateEntityCrudListHtmlFile(
  AEntityConfig: TScaffoldingEntityConfig);

  procedure ReplaceButtonNew(Source: TStrings);
  begin
    if AEntityConfig.CanInsert then
      Source.Text := StringReplace(Source.Text, '<!-- {BUTTONNEW} -->',
      '<button id="view.{ENTITY_LOWERCASE}.list.newbutton" class="btn btn-primary"></button>'#13#10 +
      '<br /><br />', [rfReplaceAll, rfIgnoreCase])
    else
      Source.Text := StringReplace(Source.Text, '<!-- {BUTTONNEW} -->', '', [rfReplaceAll, rfIgnoreCase]);
  end;

var
  Entity: string;
  HtmlFile: TStrings;
begin
  HtmlFile := TStringList.Create;
  try
    Entity := AEntityConfig.EntitySet.Name;
    HtmlFile.LoadFromFile(GetSourceFileName('', 'View.Crud.List.html'));

    ReplaceButtonNew(HtmlFile);

    HtmlFile.Text := StringReplace(HtmlFile.Text, '{ENTITY}', Entity, [rfReplaceAll, rfIgnoreCase]);
    HtmlFile.Text := StringReplace(HtmlFile.Text, '{ENTITY_LOWERCASE}', LowerCase(Entity), [rfReplaceAll, rfIgnoreCase]);

    HtmlFile.SaveToFile(GetDestinationFileName('', Format('View.%s.List.html', [Entity])));
  finally
    HtmlFile.Free;
  end;
end;

procedure TScaffoldingSourceGenerator.GenerateEntityCrudListPasFile(
  AEntityConfig: TScaffoldingEntityConfig);

  procedure ReplaceButtonNew(Source: TStrings);
  begin
    if AEntityConfig.CanInsert then
    begin
      Source.Text := StringReplace(Source.Text, '{BUTTONNEWFIELD}', '    btNew: TWebButton;'#13#10, [rfReplaceAll, rfIgnoreCase]);
      Source.Text := StringReplace(Source.Text, '{BUTTONNEWCLICKEVENTDECLARATION}', '    procedure btNewClick(Sender: TObject);'#13#10, [rfReplaceAll, rfIgnoreCase]);
      Source.Text := StringReplace(Source.Text, '{BUTTONNEWCLICKEVENTIMPLEMENTATION}',
        'procedure TFView{ENTITY}List.btNewClick(Sender: TObject);'#13#10 +
        'begin'#13#10 +
        '  FEditProc( nil );'#13#10 +
        'end;'#13#10#13#10,
        [rfReplaceAll, rfIgnoreCase]);
    end
    else
    begin
      Source.Text := StringReplace(Source.Text, '{BUTTONNEWFIELD}', '', [rfReplaceAll, rfIgnoreCase]);
      Source.Text := StringReplace(Source.Text, '{BUTTONNEWCLICKEVENTDECLARATION}', '', [rfReplaceAll, rfIgnoreCase]);
      Source.Text := StringReplace(Source.Text, '{BUTTONNEWCLICKEVENTIMPLEMENTATION}', '', [rfReplaceAll, rfIgnoreCase]);
    end;
  end;

  procedure ReplaceEditProcField(Source: TStrings);
  begin
    if HasEditView2(AEntityConfig) then
      Source.Text := StringReplace(Source.Text, '{EDITPROCFIELD}', '    FEditProc: TEditProc;'#13#10, [rfReplaceAll, rfIgnoreCase])
    else
      Source.Text := StringReplace(Source.Text, '{EDITPROCFIELD}', '', [rfReplaceAll, rfIgnoreCase]);
  end;

  procedure ReplaceCreateFormMethodDeclaration(Source: TStrings);
  begin
    if HasEditView2(AEntityConfig) then
      Source.Text := StringReplace(Source.Text, '{CREATEFORMMETHODDECLARATION}', '    class function CreateForm(AElementID: string; AShowEditProc: TEditProc): TWebForm;'#13#10, [rfReplaceAll, rfIgnoreCase])
    else
      Source.Text := StringReplace(Source.Text, '{CREATEFORMMETHODDECLARATION}', '    class function CreateForm(AElementID: string): TWebForm;'#13#10, [rfReplaceAll, rfIgnoreCase]);
  end;

  procedure ReplaceCreateFormMethodImplementation(Source: TStrings);
  begin
    if HasEditView2(AEntityConfig) then
      Source.Text := StringReplace(Source.Text, '{CREATEFORMMETHODIMPLEMENTATION}',
        'class function TFView{ENTITY}List.CreateForm(AElementID: string;'#13#10 +
        '  AShowEditProc: TEditProc): TWebForm;'#13#10 +
        ''#13#10 +
        '  procedure AfterCreate(AForm: TObject);'#13#10 +
        '  begin'#13#10 +
        '    TFView{ENTITY}List(AForm).FEditProc := AShowEditProc;'#13#10 +
        '  end;'#13#10 +
        ''#13#10 +
        'begin'#13#10 +
        '  Application.CreateForm(TFView{ENTITY}List, AElementID, Result, @AfterCreate);'#13#10 +
        'end;'#13#10#13#10, [rfReplaceAll, rfIgnoreCase])
    else
      Source.Text := StringReplace(Source.Text, '{CREATEFORMMETHODIMPLEMENTATION}',
        'class function TFView{ENTITY}List.CreateForm(AElementID: string): TWebForm;'#13#10 +
        'begin'#13#10 +
        '  Application.CreateForm(TFView{ENTITY}List, AElementID, Result);'#13#10 +
        'end;'#13#10#13#10, [rfReplaceAll, rfIgnoreCase]);
  end;

  procedure ReplaceGetCellChildrenMethodDeclaration(Source: TStrings);
  begin
    if AEntityConfig.CanModify or AEntityConfig.CanDelete then
      Source.Text := StringReplace(Source.Text, '{GETCELLCHILDRENMETHODDECLARATION}',
        '    procedure WebDBTableControl1GetCellChildren(Sender: TObject; ACol,'#13#10 +
        '      ARow: Integer; AField: TField; AValue: string; AElement: TJSHTMLElement);'#13#10, [rfReplaceAll, rfIgnoreCase])
    else
      Source.Text := StringReplace(Source.Text, '{GETCELLCHILDRENMETHODDECLARATION}', '', [rfReplaceAll, rfIgnoreCase]);
  end;

  procedure ReplaceGetCellChildrenMethodImplementation(Source: TStrings);
  begin
    if AEntityConfig.CanModify or AEntityConfig.CanDelete then
      Source.Text := StringReplace(Source.Text, '{GETCELLCHILDRENMETHODIMPLEMENTATION}',
        'procedure TFView{ENTITY}List.WebDBTableControl1GetCellChildren(Sender: TObject;'#13#10 +
      '  ACol, ARow: Integer; AField: TField; AValue: string;'#13#10 +
      '  AElement: TJSHTMLElement);'#13#10 +
      'var'#13#10 +
      '  RecordId: JSValue;'#13#10 +
      ''#13#10 +
      '  procedure EditButtonClick(Sender: TObject);'#13#10 +
      '  begin'#13#10 +
      '    FEditProc(RecordId);'#13#10 +
      '  end;'#13#10 +
      ''#13#10 +
      'var'#13#10 +
      '  ElementID: string;'#13#10 +
      '  Button: TWebButton;'#13#10 +
      'begin'#13#10 +
      '  if ARow = 0 then'#13#10 +
      '    Exit;'#13#10 +
      ''#13#10 +
      '  if WebDBTableControl1.Columns[ACol].Title = ''Actions'' then'#13#10 +
      '  begin'#13#10 +
      '    RecordId := TCrudUtils.GetIdFromObject('#13#10 +
      '      {ENTITY}Dataset.Connection.Model.DefaultSchema.FindEntityType({ENTITY}Dataset.EntitySetName),'#13#10 +
      '      {ENTITY}Dataset.CurrentData'#13#10 +
      '    );'#13#10 +
      '    Button := TWebButton.Create(nil);'#13#10 +
      '    Button.Caption := ''Edit'';'#13#10 +
      '    Button.OnClick := @EditButtonClick;'#13#10 +
      '    Button.ElementClassName := ''btn btn-outline btn-default btn-xs'';'#13#10 +
      '    Button.ParentElement := AElement;'#13#10 +
      '    Button.WidthStyle := TSizeStyle.ssAuto;'#13#10 +
      '    Button.HeightStyle := TSizeStyle.ssAuto;'#13#10 +
      '  end;'#13#10 +
      'end;'#13#10#13#10, [rfReplaceAll, rfIgnoreCase])
    else
      Source.Text := StringReplace(Source.Text, '{GETCELLCHILDRENMETHODIMPLEMENTATION}', '', [rfReplaceAll, rfIgnoreCase]);
  end;

var
  Entity: string;
  PasFile: TStrings;
  DatasetFields: TStrings;
begin
  PasFile := TStringList.Create;
  DatasetFields := TStringList.Create;
  try
    Entity := AEntityConfig.EntitySet.Name;
    PasFile.LoadFromFile(GetSourceFileName('', 'View.Crud.List.pas'));

    ReplaceButtonNew(PasFile);
    ReplaceEditProcField(PasFile);
    ReplaceCreateFormMethodDeclaration(PasFile);
    ReplaceCreateFormMethodImplementation(PasFile);
    ReplaceGetCellChildrenMethodDeclaration(PasFile);
    ReplaceGetCellChildrenMethodImplementation(PasFile);    

    AddPasDataset(AEntityConfig.EntitySet.EntityType.Name, AEntityConfig.EntitySet.EntityType, DatasetFields, True);

    PasFile.Text := StringReplace(PasFile.Text, '{DATASETFIELDS}', DatasetFields.Text, [rfReplaceAll, rfIgnoreCase]);

    PasFile.Text := StringReplace(PasFile.Text, '{ENTITY}', Entity, [rfReplaceAll, rfIgnoreCase]);
    
    PasFile.SaveToFile(GetDestinationFileName('', Format('View.%s.List.pas', [Entity])));
  finally
    PasFile.Free;
    DatasetFields.Free;
  end;
end;

function TScaffoldingSourceGenerator.GetDescriptionProperty(
  AEntityType: TXDataEntityType): TXDataProperty;
var
  Prop, TempProp: TXDataProperty;
begin
  Result := nil;
  TempProp := nil;
  for Prop in AEntityType.Properties do
  begin
    if AEntityType.Key.Contains(Prop) then
      Continue;
    if not Prop.IsSimple then
      Continue;

    if Prop.Required then
    begin
      Result := Prop;
      Break;
    end
    else
    begin
      if TempProp = nil then
        TempProp := Prop;
    end;
  end;
  if (Result = nil) and (TempProp <> nil) then
    Result := TempProp;
end;

function TScaffoldingSourceGenerator.GetDestinationFileName(SourcePath,
  SourceFile: string): string;
var
  DestinationPath: string;
begin
  DestinationPath := FConfig.ProjectDestinationPath;
  DestinationPath := TPath.Combine(DestinationPath, SourcePath );

  if Copy(DestinationPath, Length(DestinationPath), 1) <> '\' then
    DestinationPath := DestinationPath + '\';

  Result := DestinationPath + SourceFile;
end;

function TScaffoldingSourceGenerator.GetFieldType(
  AProp: TXDataProperty): string;
var
  Kind: TXTypeKind;
begin
  Kind := TXTypeKind.xtUndefined;

  if AProp.IsSimple then
  begin
    Kind := TXDataSimpleProperty(AProp).TypeKind;
  end;

  case Kind of
    TXTypeKind.xtInt16: Result := 'TSmallintField';
    TXTypeKind.xtInt32: Result := 'TIntegerField';
    TXTypeKind.xtInt64: Result := 'TLargeIntField';
    TXTypeKind.xtDouble: Result := 'TFloatField';
    TXTypeKind.xtBoolean: Result := 'TBooleanField';
    TXTypeKind.xtDate: Result := 'TDateField';
    TXTypeKind.xtDateTime: Result := 'TDateTimeField';
    TXTypeKind.xtTime: Result := 'TTimeField';
    TXTypeKind.xtCurrency: Result := 'TCurrencyField';
    TXTypeKind.xtGuid: Result := 'TGuidField';
    TXTypeKind.xtVariant: Result := 'TVariantField';
    TXTypeKind.xtByte: Result := 'TByteField';
    TXTypeKind.xtEnum: Result := 'TIntegerField';
    TXTypeKind.xtText: Result := 'TStringField';
  else
    raise EXDataScaffoldingException.Create('unsupported type');
  end;

//TXTypeKind = (
//    xtUndefined,
//    xtBinary,
//    xtStream,
//    xtSByte,
//    xtEntity,
//    xtEntityCollection,
//    xtScalarCollection
//  );
end;

function TScaffoldingSourceGenerator.GetSourceFileName(SourcePath,
  SourceFile: string): string;
begin
  Result := TPath.Combine(FConfig.TemplatePath, SourcePath) + SourceFile;
end;

function TScaffoldingSourceGenerator.HasDescriptionProperty(
  AEntityType: TXDataEntityType): Boolean;
begin
  Result := GetDescriptionProperty(AEntityType) <> nil;
end;

function TScaffoldingSourceGenerator.HasEditView2(
  AEntityConfig: TScaffoldingEntityConfig): Boolean;
begin
  Result := AEntityConfig.CanInsert or
            AEntityConfig.CanModify or
            AEntityConfig.CanDelete;
end;

function TScaffoldingSourceGenerator.IsSupportedFieldType(
  AProp: TXDataProperty): boolean;
var
  Kind: TXTypeKind;
begin
  Kind := TXTypeKind.xtUndefined;

  if AProp is TXDataNavigationProperty then
  begin
    if GetDescriptionProperty(TXDataNavigationProperty(AProp).Target) = nil then
      Exit(False);
    Kind := TXDataSimpleProperty(GetDescriptionProperty(TXDataNavigationProperty(AProp).Target)).TypeKind;
  end
  else if AProp.IsSimple then
    Kind := TXDataSimpleProperty(AProp).TypeKind;

  Result := Kind in [
    TXTypeKind.xtInt16,
    TXTypeKind.xtInt32,
    TXTypeKind.xtInt64,
    TXTypeKind.xtDouble,
    TXTypeKind.xtBoolean,
    TXTypeKind.xtDate,
    TXTypeKind.xtDateTime,
    TXTypeKind.xtTime,
    TXTypeKind.xtCurrency,
    TXTypeKind.xtGuid,
    TXTypeKind.xtVariant,
    TXTypeKind.xtByte,
    TXTypeKind.xtEnum,
    TXTypeKind.xtText,
    TXTypeKind.xtEntity
  ];
end;

end.
