Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
81830a5
Add international telephone text field editor
MikeAlhayek Jun 6, 2026
9a98395
Use Resources module
MikeAlhayek Jun 6, 2026
72d53d3
Merge origin/main into mikealhayek/adding-editors, resolving conflicts
Copilot Jun 6, 2026
b01cabe
Fix flags
MikeAlhayek Jun 6, 2026
701c812
fix view
MikeAlhayek Jun 6, 2026
b381dd2
Fix intl-tel-input flags and add CDN SRI hashes
MikeAlhayek Jun 6, 2026
0a86e1f
Fix corrupted webp flag images in gulp copy pipeline
MikeAlhayek Jun 6, 2026
edfc874
Add PhoneField custom content field with intl-tel-input integration
MikeAlhayek Jun 6, 2026
7eea30f
Add InitialCountryMode to PhoneField settings
MikeAlhayek Jun 6, 2026
ad08d6a
Add deferred data migration for PhoneNumberInfoPart.Number
MikeAlhayek Jun 6, 2026
b9dcf67
Fix phone number data migration to use memory-efficient batching
MikeAlhayek Jun 6, 2026
f1a7a2e
improve migration
MikeAlhayek Jun 6, 2026
40b3ab0
Remove unused page variable from data migration
MikeAlhayek Jun 6, 2026
08d0a9f
Fix migration to traverse BagPart for nested PhoneNumber items
MikeAlhayek Jun 6, 2026
bf88d5b
cleanup
MikeAlhayek Jun 6, 2026
b0b0035
Merge branch 'main' into mikealhayek/adding-editors
MikeAlhayek Jun 6, 2026
24340b8
Fix ContentTransfer migration missing Direction column for v1 tenants
MikeAlhayek Jun 6, 2026
24ddd02
cleanup
MikeAlhayek Jun 6, 2026
9420229
cleanup
MikeAlhayek Jun 6, 2026
4ffe26e
Make migrations idempotent for duplicate column/index errors
MikeAlhayek Jun 6, 2026
c4ad200
Initialize phone field editor for dynamically added BagPart items
MikeAlhayek Jun 7, 2026
038ab57
fix migration
MikeAlhayek Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@
# Keep LF line endings in webroot assets files. Otherwise, building them under Windows would change the line endings to CLRF and cause changes without actually editing the source files.
**/wwwroot/**/*.js text eol=lf
**/wwwroot/**/*.css text eol=lf

# Ensure binary files are never treated as text.
*.webp binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
1 change: 1 addition & 0 deletions CrestApps.OrchardCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<Project Path="src/Modules/CrestApps.OrchardCore.AI/CrestApps.OrchardCore.AI.csproj" />
<Project Path="src/Modules/CrestApps.OrchardCore.AzureAIInference/CrestApps.OrchardCore.AzureAIInference.csproj" />
<Project Path="src/Modules/CrestApps.OrchardCore.ContentAccessControl/CrestApps.OrchardCore.ContentAccessControl.csproj" />
<Project Path="src/Modules/CrestApps.OrchardCore.ContentFields/CrestApps.OrchardCore.ContentFields.csproj" />
<Project Path="src/Modules/CrestApps.OrchardCore.ContentTransfer/CrestApps.OrchardCore.ContentTransfer.csproj" />
<Project Path="src/Modules/CrestApps.OrchardCore.ContentTransfer.OpenXml/CrestApps.OrchardCore.ContentTransfer.OpenXml.csproj" />
<Project Path="src/Modules/CrestApps.OrchardCore.DncRegistry.Azure/CrestApps.OrchardCore.DncRegistry.Azure.csproj" />
Expand Down
2 changes: 1 addition & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ function buildJsPipeline(assetGroup, doConcat, doRebuild) {
}

function buildCopyPipeline(assetGroup, doRebuild) {
var stream = gulp.src(assetGroup.inputPaths);
var stream = gulp.src(assetGroup.inputPaths, { encoding: false });

if (!doRebuild) {
stream = stream.pipe(newer(assetGroup.outputDir))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<ProjectReference Include="../../Abstractions/CrestApps.OrchardCore.Abstractions/CrestApps.OrchardCore.Abstractions.csproj" />
<ProjectReference Include="../../Modules/CrestApps.OrchardCore.ContentFields/CrestApps.OrchardCore.ContentFields.csproj" />
<ProjectReference Include="../CrestApps.OrchardCore.YesSql.Core/CrestApps.OrchardCore.YesSql.Core.csproj" />
<ProjectReference Include="..\CrestApps.OrchardCore.Core\CrestApps.OrchardCore.Core.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using OrchardCore.ContentFields.Fields;
using CrestApps.OrchardCore.ContentFields.Fields;
using OrchardCore.ContentFields.Fields;
using OrchardCore.ContentManagement;

namespace CrestApps.OrchardCore.Omnichannel.Core.Models;
Expand All @@ -11,7 +12,7 @@ public sealed class PhoneNumberInfoPart : ContentPart
/// <summary>
/// Gets or sets the number.
/// </summary>
public TextField Number { get; set; }
public PhoneField Number { get; set; }

/// <summary>
/// Gets or sets the extension.
Expand Down
3 changes: 3 additions & 0 deletions src/CrestApps.Docs/docs/changelog/v2.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ Large parts of the reusable AI infrastructure are no longer implemented only ins
- Azure AI Search index editors for **AI Documents** and **AI Memory** now normalize their built-in managed mappings on load and update so repeated edits do not keep appending duplicate `Content`, `Embedding`, and related managed fields, while custom mappings remain preserved
- The **Clear saved AI memory** action in the current user's profile editor now uses Orchard Core's standard admin confirmation dialog and clears the user's saved memory entries by filtering the store's persisted records for the current user before removing the corresponding indexed AI memory documents
- Content Transfer content-type settings now default **Allow Bulk Import** and **Allow Bulk Export** to enabled, so content types participate by default and can explicitly opt out by turning either setting off
- Added a new `CrestApps.OrchardCore.ContentFields` module with a `PhoneField` content field that stores a phone number in E.164 format together with the ISO country code and national number, uses `intl-tel-input` for country-aware editing, and validates input through `IPhoneNumberService`
- Omnichannel Management now migrates `PhoneNumberInfoPart.Number` from a `TextField` to a `PhoneField` so phone numbers persist with the correct country flag
- Moved the shared `intl-tel-input` library assets and Orchard resource-manager registration into `CrestApps.OrchardCore.Resources`, while `CrestApps.OrchardCore.ContentFields` depends on that feature for the phone editor
- Content Transfer now keeps CSV support in the base `CrestApps.OrchardCore.ContentTransfer` feature and moves optional `.xlsx` support into `CrestApps.OrchardCore.ContentTransfer.OpenXml`, with the import and export UI showing only the file extensions enabled by the current tenant feature set
- Content Transfer now welds lazily created parts onto the parent content item during column discovery, import, and export so `.xlsx` exports no longer fail with a `System.Text.Json` node-cycle exception when a content item does not already materialize one of its configured parts
- Content Transfer imports now commit inline status changes before their deferred background jobs run, use **Pending**, **Paused**, and **Deleting** import states in the admin list, surface **Resume import** and **Pause import** actions instead of the earlier cancel/process wording, and migrate older canceled import rows to the paused state
Expand Down
78 changes: 78 additions & 0 deletions src/CrestApps.Docs/docs/modules/content-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
sidebar_label: Content Fields
sidebar_position: 2
title: Content Fields
description: Adds custom Orchard Core content fields maintained by CrestApps.
---

| | |
| --- | --- |
| **Feature Name** | CrestApps Content Fields |
| **Feature ID** | `CrestApps.OrchardCore.ContentFields` |

Provides custom Orchard Core content fields maintained by CrestApps.

## Overview

This module adds custom content fields for Orchard Core that extend the built-in field library with additional functionality. Each field ships with its own display driver, settings, edit and display views.

## Included fields

### PhoneField

A content field that stores an international phone number together with its ISO country code so the correct country flag is always displayed when the field is edited again.

The field uses the [intl-tel-input](https://intl-tel-input.com/) library (provided by `CrestApps.OrchardCore.Resources`) to give editors a country-aware phone number input with flag dropdown and automatic formatting.

#### Stored properties

| Property | Type | Description |
| --- | --- | --- |
| `PhoneNumber` | `string` | The full phone number in E.164 format (e.g. `+14155552671`). |
| `CountryCode` | `string` | ISO 3166-1 alpha-2 country code (e.g. `US`, `CA`). Stored separately because some countries share a calling code (e.g. US and CA both use `+1`). |
| `NationalNumber` | `string` | The national (local) portion of the number without the country calling code (e.g. `4155552671`). |

#### Settings

| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `Hint` | `string` | `null` | Help text displayed below the field. |
| `Required` | `bool` | `false` | Whether the field is required. |
| `InitialCountryMode` | `InitialCountryMode` | `Globe` | Controls which country flag is pre-selected when the field is empty. See [Initial country modes](#initial-country-modes). |
| `SpecificCountryCode` | `string` | `null` | ISO country code used when `InitialCountryMode` is `Specific` (e.g. `US`). |

#### Initial country modes

| Mode | Behavior |
| --- | --- |
| **Globe** | Shows the globe icon without pre-selecting any country. This is the default. |
| **Current culture** | Resolves the country from the current request culture's region (e.g. `en-US` resolves to `US`). |
| **Specific** | Always pre-selects the country configured in the **Country** dropdown. |

#### Adding PhoneField via migration

```csharp
await _contentDefinitionManager.AlterPartDefinitionAsync("MyPart", part => part
.WithField("Phone", field => field
.OfType("PhoneField")
.WithDisplayName("Phone Number")
.WithPosition("1")
.WithSettings(new PhoneFieldSettings
{
Required = true,
InitialCountryMode = InitialCountryMode.Specific,
SpecificCountryCode = "US",
Hint = "Enter a phone number with country code.",
})
)
);
```

#### Server-side validation

When the field value is submitted, the display driver uses `IPhoneNumberService` (from `CrestApps.OrchardCore.PhoneNumbers`) to validate that the entered number is a well-formed phone number. Invalid numbers produce a model-state error and the editor is re-displayed.

## Notes

- The shared `intl-tel-input` script and stylesheet are registered by `CrestApps.OrchardCore.Resources`.
- The Omnichannel Management module depends on this feature for `PhoneNumberInfoPart.Number`.
1 change: 1 addition & 0 deletions src/CrestApps.Docs/docs/modules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ CrestApps provides a set of standard modules that enhance core Orchard Core CMS
| Module | Feature ID | Description |
|--------|-----------|-------------|
| [Content Access Control](content-access-control) | `CrestApps.OrchardCore.ContentAccessControl` | Role-based content access restrictions |
| [Content Fields](content-fields) | `CrestApps.OrchardCore.ContentFields` | Custom Orchard Core content field editors |
| [Content Transfer](content-transfer) | `CrestApps.OrchardCore.ContentTransfer` | Bulk Excel import and export for content items |
| [DNC Registry](dnc-registry) | `CrestApps.OrchardCore.DncRegistry` | National do-not-call registry integrations and import compliance settings |
| [Recipes](recipes) | `CrestApps.OrchardCore.Recipes` | JSON-Schema support for Orchard Core recipes |
Expand Down
8 changes: 8 additions & 0 deletions src/CrestApps.Docs/docs/modules/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ Provides shared resources and libraries used by various CrestApps modules.
This module provides shared frontend resources (CSS and JavaScript) that are used by other CrestApps modules. It acts as a central resource library, ensuring consistent styling and behavior across the CrestApps module ecosystem.

Other CrestApps modules declare a dependency on this feature to leverage common scripts and stylesheets without duplicating assets.

## Shared libraries

This feature registers reusable Orchard resource-manager assets that can be consumed by other CrestApps modules.

Current shared libraries include:

- `intl-tel-input` script and stylesheet resources, backed by local copied assets with CDN fallbacks
2 changes: 2 additions & 0 deletions src/CrestApps.Docs/docs/omnichannel/management.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ In Orchard Core Admin:
4. Add any fields/parts you need (phone number, email, lead status, custom fields, etc.).
5. Create/import contact items.

If you use the built-in `PhoneNumberInfoPart`, the `Number` field is a `PhoneField` (from `CrestApps.OrchardCore.ContentFields`) that stores the phone number in E.164 format alongside the ISO country code, so the correct country flag is always displayed when the field is edited again.

When a content type includes `OmnichannelContactPart`, the module now enforces two code-controlled omnichannel surfaces:

- `OmnichannelContactPart` stores the contact-level communication compliance flags (`DoNotCall`, `DoNotSms`, `DoNotEmail`, `DoNotChat`) and their UTC timestamps.
Expand Down
1 change: 1 addition & 0 deletions src/CrestApps.Docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ const sidebars = {
items: [
'modules/index',
'modules/content-access-control',
'modules/content-fields',
'modules/recipes',
'modules/resources',
'modules/roles',
Expand Down
14 changes: 14 additions & 0 deletions src/Modules/CrestApps.OrchardCore.ContentFields/Assets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"inputs": [
"Assets/js/international-telephone-editor.js"
],
"output": "wwwroot/scripts/international-telephone-editor.js"
},
{
"inputs": [
"Assets/css/international-telephone-editor.css"
],
"output": "wwwroot/styles/international-telephone-editor.css"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.content-field-international-telephone .iti,
.content-field-international-telephone .iti--inline-dropdown,
.content-field-international-telephone .iti__tel-input {
width: 100%;
}

.content-field-international-telephone .iti {
display: block;
}

.content-field-international-telephone .iti__tel-input {
min-height: calc(1.5em + 0.75rem + 2px);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
(function () {
var selector = '[data-phone-field]';

function initializeField(wrapper) {
var telInput = wrapper.querySelector('input[data-intl-tel-input="true"]');

if (!window.intlTelInput || !telInput || telInput.dataset.intlTelInputInitialized === 'true') {
return;
}

telInput.dataset.intlTelInputInitialized = 'true';

var e164Input = wrapper.querySelector('[data-phone-e164]');
var countryInput = wrapper.querySelector('[data-phone-country]');
var nationalInput = wrapper.querySelector('[data-phone-national]');

var options = {
containerClass: 'w-100',
dropdownParent: document.body,
numberDisplayFormat: 'INTERNATIONAL',
strictMode: true
};

var initialCountry = telInput.dataset.initialCountry;

if (initialCountry) {
options.initialCountry = initialCountry;
}

var telephoneInput = window.intlTelInput(telInput, options);

if (telInput.disabled) {
telephoneInput.setDisabled(true);
}
else if (telInput.readOnly) {
telephoneInput.setReadonly(true);
}

if (telInput.form) {
telInput.form.addEventListener('submit', function () {
if (!telInput.value) {
if (e164Input) {
e164Input.value = '';
}

if (countryInput) {
countryInput.value = '';
}

if (nationalInput) {
nationalInput.value = '';
}

return;
}

var e164Number = telephoneInput.getNumber();
var countryData = telephoneInput.getSelectedCountryData();

if (e164Input) {
e164Input.value = e164Number || '';
}

if (countryInput) {
countryInput.value = (countryData.iso2 || '').toUpperCase();
}

if (nationalInput) {
nationalInput.value = telInput.value || '';
}
});
}
}

function initialize() {
document.querySelectorAll(selector).forEach(initializeField);
}

function observeDynamicAdditions() {
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var addedNodes = mutations[i].addedNodes;

for (var j = 0; j < addedNodes.length; j++) {
var node = addedNodes[j];

if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}

if (node.matches(selector)) {
initializeField(node);
}
else {
var fields = node.querySelectorAll(selector);
fields.forEach(initializeField);
}
}
}
});

observer.observe(document.body, { childList: true, subtree: true });
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
initialize();
observeDynamicAdditions();
}, { once: true });

return;
}

initialize();
observeDynamicAdditions();
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<RootNamespace>$(MSBuildProjectName)</RootNamespace>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Title>CrestApps OrchardCore Content Fields Module</Title>
<Description>
$(CrestAppsDescription)

Adds custom Orchard Core content fields maintained by CrestApps.
</Description>
<PackageTags>$(PackageTags) OrchardCoreCMS Content Fields</PackageTags>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="OrchardCore.Module.Targets" />
<PackageReference Include="OrchardCore.ContentFields" />
<PackageReference Include="OrchardCore.ContentTypes.Abstractions" />
<PackageReference Include="OrchardCore.ResourceManagement" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../Abstractions/CrestApps.OrchardCore.Abstractions/CrestApps.OrchardCore.Abstractions.csproj" />
<ProjectReference Include="../../Abstractions/CrestApps.OrchardCore.PhoneNumbers.Abstractions/CrestApps.OrchardCore.PhoneNumbers.Abstractions.csproj" />
<ProjectReference Include="../CrestApps.OrchardCore.PhoneNumbers/CrestApps.OrchardCore.PhoneNumbers.csproj" PrivateAssets="none" />
<ProjectReference Include="../CrestApps.OrchardCore.Resources/CrestApps.OrchardCore.Resources.csproj" PrivateAssets="none" />
</ItemGroup>

</Project>
Loading
Loading