Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,19 @@ App-specific behavior:
| `src/api/graph.js` | Graph calendar fetch for attendee prefill |
| `src/api/skyline.js` | Skyline Collaboration API — proxied via DMA automation script to avoid CORS |
| `src/api/subscriptions.js` | Subscription CRUD via DataMiner Automation (DOM-backed) |
| `src/api/mailto.js` | Helpers to open a prefilled email (`mailto:`) for the standalone forms |
| `src/api/leads.js` | Opens a prefilled lead email in the user's mail client for manual sending |
| `src/api/opportunities.js` | Opens a prefilled opportunity email in the user's mail client for manual sending |
| `src/authConfig.js` | MSAL config, scopes for Dataverse/Graph/Skyline |
| `src/components/AuthGuard.jsx` | Triple-auth gate: DMA session → MSAL ssoSilent/popup → WhoAmI |
| `src/components/ActivityForm.jsx` | Activity creation (4 types, account/attendee pickers, calendar) |
| `src/components/NotesList.jsx` | Browse view — lazy server-side OData filters |
| `src/components/SubscriptionsPanel.jsx` | Email notification subscription management |
| `src/components/forms/FormPage.jsx` | Generic shell for standalone forms (header + back button); renders forms from the registry |
| `src/components/forms/LeadForm.jsx` | "Add lead" form shown to users without Dynamics/Dataverse access |
| `src/components/forms/OpportunityForm.jsx` | "Add opportunity" form shown to users without Dynamics/Dataverse access |
Comment on lines +164 to +166
| `src/forms/registry.js` | Registry of standalone forms — add new forms here (future-proof) |
| `src/hooks/useHashRoute.js` | Minimal hash-based router (`#/forms/<id>`) for standalone form pages |
| `src/components/AutocompletePicker.jsx` | Debounced autocomplete with clearOnPick support |
| `src/components/CalendarPicker.jsx` | Graph calendar modal (60d past + 30d future) |
| `src/hooks/useTamContext.js` | TAM account context — loads managed accounts from Skyline API |
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/deploy-dma-on-pr-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ on:
workflow_dispatch:
inputs:
deployment_target:
description: "Deployment target"
description: >
Target environment for a manual deploy. 'dev' installs to the
/public/DynamicsActivitiesDev/ folder and leaves production
untouched. 'production' installs to /public/DynamicsActivities/.
PR merges to main always deploy to production.
required: true
default: "production"
type: choice
options:
- production
- dev
default: dev

permissions:
contents: read
Expand All @@ -48,7 +52,9 @@ jobs:
agent_destination_id: "e21dbc3e-7592-48f8-867b-d81d4ff06bee"
vite_redirect_uri: ${{ vars.VITE_REDIRECT_URI }}
vite_app_base_path: "/public/DynamicsActivities/"
app_base_path: "/public/DynamicsActivities/"
webapp_public_folder: "DynamicsActivities"
version_suffix: ""
secrets: inherit

deploy_release_candidate:
Expand All @@ -70,5 +76,7 @@ jobs:
agent_destination_id: "e21dbc3e-7592-48f8-867b-d81d4ff06bee"
vite_redirect_uri: ${{ vars.VITE_REDIRECT_URI_DEV }}
vite_app_base_path: "/public/DynamicsActivitiesDev/"
app_base_path: "/public/DynamicsActivitiesDev/"
webapp_public_folder: "DynamicsActivitiesDev"
version_suffix: "-dev"
secrets: inherit
30 changes: 29 additions & 1 deletion .github/workflows/deploy-dmapp-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ on:
type: string
default: ""

app_base_path:
description: >
Public base path the SPA is served from (e.g. /public/MyApp/).
Passed to the frontend build as VITE_APP_BASE_PATH so asset URLs
and MSAL redirect paths resolve correctly. Leave empty to use the
app's built-in default.
required: false
type: string
default: ""
Comment on lines +95 to +103

webapp_public_folder:
description: >
Folder name under C:\Skyline DataMiner\Webpages\public where the
Expand All @@ -115,6 +125,23 @@ on:
type: string
default: "/public/DynamicsActivities/"

dotnet_build_properties:
description: >
Extra MSBuild -p: properties appended to the DataMiner package build
(e.g. -p:WebAppPublicFolder=MyAppDev). Space-separated.
required: false
type: string
default: ""

version_suffix:
description: >
Optional suffix appended to the package/catalog version (e.g. -dev)
so non-production builds don't overwrite the production catalog
artifact.
required: false
type: string
default: ""

secrets:
DATAMINER_TOKEN:
description: DataMiner system token for Catalog upload and deployment.
Expand Down Expand Up @@ -237,14 +264,15 @@ jobs:
Write-Error "Add <Version>X.Y.Z</Version> to the .csproj PropertyGroup."
exit 1
}
$version = "$version" + "${{ inputs.version_suffix }}"
"number=$version" >> $env:GITHUB_OUTPUT
Write-Host "Package version: $version"

# --------------------------------------------------------
# 7. Build DataMiner package (.dmapp)
# --------------------------------------------------------
- name: Build DataMiner package
run: dotnet build "${{ steps.csproj.outputs.path }}" -c Release -p:WebAppPublicFolder="${{ inputs.webapp_public_folder }}"
run: dotnet build "${{ steps.csproj.outputs.path }}" -c Release -p:WebAppPublicFolder="${{ inputs.webapp_public_folder }}" ${{ inputs.dotnet_build_properties }}

# --------------------------------------------------------
# 8. Locate generated .dmapp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<GenerateDataMinerPackage>True</GenerateDataMinerPackage>
<MinimumRequiredDmVersion>10.3.0.0 - 12752</MinimumRequiredDmVersion>
<!-- <MinimumRequiredDmWebVersion>10.6.2 (CU0)</MinimumRequiredDmWebVersion> -->
<Version>1.8.85</Version>
<VersionComment>Fix sync workflow startup configuration</VersionComment>
<Version>1.12.5</Version>
<VersionComment>Fix: lead/opportunity forms no longer open multiple email drafts on a fast double-click or Enter+click. Added a synchronous submit guard so the mail draft opens exactly once per submission.</VersionComment>
Comment on lines +11 to +12
<!-- Do NOT fill in the Organization Token here. This points to either an Environment Variable skyline__sdk__dataminertoken holding the organization token or a Visual Studio User Secret holding the token. -->
<CatalogPublishKeyName>skyline:sdk:dataminertoken</CatalogPublishKeyName>
<CatalogDefaultDownloadKeyName>skyline:sdk:dataminertoken</CatalogDefaultDownloadKeyName>
Expand All @@ -32,12 +32,17 @@
C:\Skyline DataMiner\Webpages\public\<WebAppPublicFolder>
-->
<PropertyGroup>
<!-- Public web folder the app is served from (…\Webpages\public\<folder>).
Defaults to the production folder; override for a dev deploy with
-p:WebAppPublicFolder=DynamicsActivitiesDev. -->
<WebAppPublicFolder Condition="'$(WebAppPublicFolder)' == ''">DynamicsActivities</WebAppPublicFolder>
<!-- Repository root (two levels up) where package.json and vite.config.js live. -->
<WebAppRoot>$(MSBuildProjectDirectory)\..\..</WebAppRoot>
<WebAppDistDir>$(WebAppRoot)\dist-dataminer</WebAppDistDir>
<!-- Default install folder for production; can be overridden via /p:WebAppPublicFolder=... -->
<WebAppPublicFolder Condition="'$(WebAppPublicFolder)' == ''">DynamicsActivities</WebAppPublicFolder>
<WebAppCompanionDir>$(MSBuildProjectDirectory)\PackageContent\CompanionFiles\Skyline DataMiner\Webpages\public\$(WebAppPublicFolder)</WebAppCompanionDir>
<WebAppCompanionRoot>$(MSBuildProjectDirectory)\PackageContent\CompanionFiles\Skyline DataMiner\Webpages\public</WebAppCompanionRoot>
<WebAppCompanionDir>$(WebAppCompanionRoot)\$(WebAppPublicFolder)</WebAppCompanionDir>
Comment on lines +35 to +45
</PropertyGroup>

<!-- If the dist-dataminer build output is missing, generate it with npm (npm install + npm run build). -->
Expand All @@ -53,8 +58,9 @@
<DynamicsActivitiesWebAppFiles Include="$(WebAppDistDir)\**\*" />
</ItemGroup>
<Error Condition="'@(DynamicsActivitiesWebAppFiles)' == ''" Text="No files found in '$(WebAppDistDir)'. Run 'npm install' and 'npm run build' in '$(WebAppRoot)' to generate the dist-dataminer output." />
<!-- Clean the destination first so removed/renamed assets don't linger. -->
<RemoveDir Directories="$(WebAppCompanionDir)" />
<!-- Clean the whole public folder first so removed/renamed assets don't linger
and only the selected public folder (prod OR dev) ends up in the package. -->
<RemoveDir Directories="$(WebAppCompanionRoot)" />
<MakeDir Directories="$(WebAppCompanionDir)" />
<Copy SourceFiles="@(DynamicsActivitiesWebAppFiles)" DestinationFiles="@(DynamicsActivitiesWebAppFiles->'$(WebAppCompanionDir)\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
<Message Importance="high" Text="Copied web app files from '$(WebAppDistDir)' into companion folder '$(WebAppCompanionDir)'." />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# The DynamicsActivities web app is generated at build time from the repository
# root 'dist-dataminer' output and copied here by the DynamicsActivitiesPackage
# project. It is a build artifact and should not be committed.
Webpages/public/DynamicsActivities/
# project. It is a build artifact and should not be committed. Covers both the
# production (DynamicsActivities) and dev (DynamicsActivitiesDev) folders.
Webpages/public/
40 changes: 14 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ The repository also contains creation/import components (`ActivityForm`, `InboxT
| Activities tab | Search and browse Dynamics data with filters, rich previews, direct Dynamics links, and Assistant-generated timeline highlights |
| Browse views | `Activities`, `Opportunities`, `Leads`, and `Support` |
| Subscriptions tab | Create, edit, pause, resume, and delete DataMiner DOM-backed notification subscriptions |
| Access gate | Request-license / request-access screen plus standalone **Add lead** and **Add opportunity** entry points |
| Standalone forms | `#/forms/lead` and `#/forms/opportunity` render outside the Dynamics auth gate |
| Access gate | Request-license / request-access screen for users without Dynamics access |
| Standalone forms | `#/forms/lead` and `#/forms/opportunity` render the **Add lead** / **Add opportunity** forms, opened from the Leads / Opportunities browse views by Team Member CAL users |
Comment on lines +23 to +24

### Activities tab details

Expand Down Expand Up @@ -76,7 +76,7 @@ Rules reflected in the current code:

- Sales / Team Member-capable Dynamics licenses are accepted
- Pure Dataverse/CDS-only SKU families such as `DYN365_CDS_*` are explicitly rejected
- Users without usable access are shown a request-access screen and can still open the standalone **lead** and **opportunity** forms
- Users without usable access are shown a request-access screen
- On localhost, the DataMiner session check is skipped and popup-based MSAL auth is used directly

### Redirect behavior
Expand All @@ -87,6 +87,10 @@ Rules reflected in the current code:

`src/authConfig.js` derives the callback from `VITE_APP_BASE_PATH` when `VITE_REDIRECT_URI` is not explicitly supplied.

### License test control (local + dev only)

A small widget is pinned to the bottom-left corner on **localhost** and the **dev deployment** (`/public/DynamicsActivitiesDev/`). It shows the Dynamics license / CAL type detected at sign-in and provides a **"View as" dropdown** to force each possible view — no Dynamics license, Dynamics without Dataverse access, Team Member, or Sales — so testers can preview what each user type sees. The selection is persisted in `localStorage`. `isTestEnvironment()` in `src/api/dataminer.js` gates it, so it never renders on production or RC. Note that `systemuser.caltype` is only reliably populated on-premises; in Dynamics 365 Online it may default to `0` (Professional), which is another reason the override is useful.

---

## Notification subscriptions
Expand All @@ -111,14 +115,14 @@ Behavior to be aware of:

## Standalone forms

The mounted app shell does not currently expose activity creation, but it does expose two standalone forms through the auth/access flow:
The mounted app shell does not currently expose activity creation, but it exposes two standalone forms reachable via `#/forms/<id>`:

| Route | Purpose | Backend |
|---|---|---|
| `#/forms/lead` | Submit a new lead | `DynamicsActivities_SubmitLead` automation script |
| `#/forms/opportunity` | Submit a new opportunity | `DynamicsActivities_SubmitOpportunity` automation script |
| `#/forms/lead` | Submit a new lead | Opens a prefilled email in the user's mail client |
| `#/forms/opportunity` | Submit a new opportunity | Opens a prefilled email in the user's mail client |

These forms are intentionally reachable outside the normal Dynamics browse experience and depend on the **DataMiner session**, not on Dataverse access.
These forms are opened from the **Add lead** / **Add opportunity** buttons shown in the Leads and Opportunities browse views. The buttons are visible only to **Team Member CAL** users — Sales/Enterprise users create leads and opportunities directly in Dynamics. Submitting a form opens the user's own email client (via `mailto:`) with the recipient, subject and details prefilled; the user reviews and sends it manually, so no Dataverse access or server-side SMTP configuration is required.

---

Expand Down Expand Up @@ -219,23 +223,6 @@ and the frontend build base path is set to `/public/DynamicsActivitiesDev/` so s
| `.github/workflows/deploy-dma-on-pr-merge.yml` | Caller workflow for production-on-merge, manual `production` from `main` only, and manual `dev` deploys |
| `.github/workflows/deploy-dmapp-reusable.yml` | Reusable build/register/deploy workflow for DMAPP packaging and Catalog deployment |
| `.github/workflows/copilot-bug-triage.yml` | Auto-comments on `bug`-labeled issues to ask `@copilot` for investigation |
| `.github/workflows/sync-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR, enables auto-merge, asks `@copilot` to resolve conflicts, and retries when the PR is updated |

### Release candidate deployment

`release-candidate` is the integration branch for all changes that create, update, or delete Dataverse data. Every push to it deploys the app to:

```text
https://solutionsdma-skyline.on.dataminer.services/auth/?url=%2Fpublic%2FDynamicsActivitiesRC%2Findex.html
```

Open write-capable PRs with `release-candidate` selected as their base branch. When the candidate is approved, open a promotion PR from `release-candidate` into `main`; production remains deployed only when that PR merges.

The release-candidate deployment uses the repository variable `VITE_REDIRECT_URI_RC`, which must match the redirect URI registered on the deployed Entra application:

```text
https://solutionsdma-skyline.on.dataminer.services/public/DynamicsActivitiesRC/
```

### Deployment URLs

Expand Down Expand Up @@ -270,8 +257,6 @@ in `DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackag
| `DynamicsActivities_NotifySubscribers` | Scheduled / manual digest sender |
| `DynamicsActivities_Summarize` | Timeline-summary generation for browse and digest flows |
| `DynamicsActivities_SkylineApiProxy` | DMA-host proxy to Skyline API to avoid browser CORS issues |
| `DynamicsActivities_SubmitLead` | Emails submitted standalone lead forms |
| `DynamicsActivities_SubmitOpportunity` | Emails submitted standalone opportunity forms |

### Optional Assistant dependency

Expand All @@ -297,6 +282,7 @@ src/
opportunities.js # Standalone opportunity submission client
components/
AuthGuard.jsx # Access gate and no-access UX
LicenseTestControl.jsx # Local/dev-only widget to preview each license view
NotesList.jsx # Mounted browse experience
SubscriptionsPanel.jsx # Mounted subscriptions experience
SubscriptionForm.jsx # Create/edit subscription UI
Expand All @@ -309,6 +295,8 @@ src/
CalendarImportTab.jsx # Present in repo but not mounted by App.jsx
forms/
registry.js # Standalone form registry
context/
LicenseTestContext.jsx # Local/dev-only license view override state
hooks/
useHashRoute.js # Minimal hash router for forms
useTamContext.js # TAM account resolution
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dynamics-activities",
"private": true,
"version": "1.8.42",
"version": "1.9.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
13 changes: 12 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import AuthGuard from './components/AuthGuard'
import NotesList from './components/NotesList'
import useTamContext from './hooks/useTamContext'
import SubscriptionsPanel from './components/SubscriptionsPanel'
import FormPage from './components/forms/FormPage'
import useHashRoute, { navigate } from './hooks/useHashRoute'
import { signOut as dmaSignOut, isDataMinerHost, getDmaUser, redirectToAuth } from './api/dataminer'

const TABS = [
Expand Down Expand Up @@ -68,6 +70,7 @@ export default function App() {
const [activeTab, setActiveTab] = useState('browse')
const [themePref, setThemePref] = useState(getInitialTheme)
const [dmaConnection, setDmaConnection] = useState(null)
const route = useHashRoute()

// Apply theme to document
useEffect(() => {
Expand Down Expand Up @@ -100,9 +103,16 @@ function cycleTheme() {
window.open(url, '_blank', 'noopener,noreferrer')
}

// Standalone form pages (e.g. "Add lead") render outside the Dynamics/Dataverse
// auth gate so they stay reachable when the user has no Dynamics access.
if (route.startsWith('forms/')) {
const formId = route.slice('forms/'.length)
return <FormPage formId={formId} onBack={() => navigate('')} />
}

return (
<AuthGuard onDmaConnection={setDmaConnection}>
{() => (
{(currentUserId) => (
<div className="app">
{/* Header — 49px with logo */}
<header className="app-header">
Expand Down Expand Up @@ -168,6 +178,7 @@ function cycleTheme() {
initialAccount={null}
managedAccounts={managedAccounts}
tamLoading={tamLoading}
currentUserId={currentUserId}
/>
)}
{activeTab === 'subscriptions' && (
Expand Down
Loading