Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Commit messages

Generate all commit messages in **Conventional Commits** format:

```
<type>(<optional scope>): <description>

[optional body]

[optional footer(s)]
```

**Rules:**

- **Type** is required and must be one of:
- `feat` — a new feature
- `fix` — a bug fix
- `docs` — documentation only
- `style` — formatting, whitespace, no code-behavior change
- `refactor` — code change that neither fixes a bug nor adds a feature
- `perf` — a performance improvement
- `test` — adding or correcting tests
- `build` — build system or dependency changes
- `ci` — CI/CD configuration changes
- `chore` — routine maintenance, no production code change
- `revert` — reverts a previous commit
- **Scope** is optional and given in parentheses after the type (e.g. `feat(identity):`). Use a short, lowercase area name when it adds clarity.
- **Description** is a short, imperative-mood summary ("add", not "added"/"adds"), lowercase, no trailing period, ideally ≤ 72 characters.
- **Body** (optional) explains the *what* and *why*, not the *how*. Separate it from the description with one blank line.
- **Breaking changes** are indicated with a `!` before the colon (e.g. `feat!:`) and/or a `BREAKING CHANGE:` footer describing the change.
- Reference issues/PRs in the footer where relevant (e.g. `Closes #123`).

**Examples:**

```
feat(identity): add bulk user offboarding endpoint
fix(graph): handle expired token on retry
docs: update authentication model overview
refactor(standards)!: rename remediation parameter
```
2 changes: 1 addition & 1 deletion .github/workflows/Conventional_Commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const title = context.payload.pull_request.title || '';
const pattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .+/;
const pattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .+/i;

if (pattern.test(title)) {
console.log(`✓ PR title follows Conventional Commits format: "${title}"`);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cipp",
"version": "10.6.0",
"version": "10.6.1",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
Expand Down
2 changes: 1 addition & 1 deletion public/version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "10.6.0"
"version": "10.6.1"
}
18 changes: 11 additions & 7 deletions src/components/CippComponents/CippAppTemplateDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,18 @@ export const CippAppTemplateDrawer = ({
value: typeValue,
}
}
// Normalize "Save as Template" configs (IntuneBody format) to form fields
if (config.IntuneBody && !config.applicationName) {
// Normalize "Save as Template" configs to form fields, canonicalizing on the
// lowercase applicationName so deploy-time ConvertFrom-Json never sees both casings.
if (config.IntuneBody) {
const body = config.IntuneBody
config.applicationName = config.ApplicationName || body.displayName || ''
config.description = body.description || ''
config.AssignTo = config.assignTo || 'On'
if (!config.applicationName) {
config.applicationName = config.ApplicationName || body.displayName || ''
}
delete config.ApplicationName
if (!config.description) config.description = body.description || ''
if (!config.AssignTo) config.AssignTo = config.assignTo || 'On'
// WinGet/Store: packageIdentifier
if (body.packageIdentifier) {
if (!config.packagename && body.packageIdentifier) {
config.packagename = body.packageIdentifier
}
// Chocolatey: extract package name from detection rules or install command
Expand All @@ -206,7 +210,7 @@ export const CippAppTemplateDrawer = ({
if (match) config.packagename = match[1]
}
// Chocolatey: custom repo
if (body.installCommandLine) {
if (!config.customRepo && body.installCommandLine) {
const repoMatch = body.installCommandLine.match(/-CustomRepo\s+(\S+)/i)
if (repoMatch) config.customRepo = repoMatch[1]
}
Expand Down
197 changes: 112 additions & 85 deletions src/components/CippFormPages/CippAddEditUser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const CippAddEditUser = (props) => {
const [selectedTemplate, setSelectedTemplate] = useState(null)
const [displayNameManuallySet, setDisplayNameManuallySet] = useState(false)
const [usernameManuallySet, setUsernameManuallySet] = useState(false)
// Tracks the template already applied to the form so we can tell a first
// apply (fill empty fields) apart from a switch (replace/clear stale values)
const appliedTemplateKeyRef = useRef(null)
const router = useRouter()
const { userId } = router.query

Expand Down Expand Up @@ -261,6 +264,7 @@ const CippAddEditUser = (props) => {
// Only clear selected template if it's not the default template
if (selectedTemplate && !selectedTemplate.defaultForTenant) {
setSelectedTemplate(null)
appliedTemplateKeyRef.current = null
}
}
}, [
Expand Down Expand Up @@ -290,100 +294,123 @@ const CippAddEditUser = (props) => {

// Auto-populate fields when template selected
useEffect(() => {
if (formType === 'add' && watchedFields.userTemplate?.addedFields) {
const template = watchedFields.userTemplate.addedFields
setSelectedTemplate(template)
if (formType !== 'add' || !watchedFields.userTemplate?.addedFields) return
const template = watchedFields.userTemplate.addedFields
const templateKey = watchedFields.userTemplate.value ?? template.GUID ?? template.templateName

// Reset manual edit flags when template changes
setDisplayNameManuallySet(false)
setUsernameManuallySet(false)
// Distinguish the first apply from a switch. On a switch we replace
// template-driven fields (and clear ones the new template doesn't define)
// so stale values from the previous template don't linger. On the first
// apply we only fill fields that have a template value, so we don't clobber
// input the user already entered or copied from another user.
const isSwitch =
appliedTemplateKeyRef.current !== null && appliedTemplateKeyRef.current !== templateKey
appliedTemplateKeyRef.current = templateKey

// Only set fields if they don't already have values (don't override user input)
const setFieldIfEmpty = (fieldName, value) => {
if (value) {
formControl.setValue(fieldName, value)
}
}
setSelectedTemplate(template)

// Populate form fields from template
if (template.primDomain) {
// If primDomain is an object, use it as-is; if it's a string, convert to object
const primDomainValue =
typeof template.primDomain === 'string'
? { label: template.primDomain, value: template.primDomain }
: template.primDomain
formControl.setValue('primDomain', primDomainValue)
}
if (template.usageLocation) {
// Handle both object and string formats
const usageLocationCode =
typeof template.usageLocation === 'string'
? template.usageLocation
: template.usageLocation?.value
const country = countryList.find((c) => c.Code === usageLocationCode)
if (country) {
setFieldIfEmpty('usageLocation', {
label: country.Name,
value: country.Code,
})
}
}
setFieldIfEmpty('jobTitle', template.jobTitle)
setFieldIfEmpty('streetAddress', template.streetAddress)
setFieldIfEmpty('city', template.city)
setFieldIfEmpty('state', template.state)
setFieldIfEmpty('postalCode', template.postalCode)
setFieldIfEmpty('country', template.country)
setFieldIfEmpty('companyName', template.companyName)
setFieldIfEmpty('department', template.department)
setFieldIfEmpty('mobilePhone', template.mobilePhone)
const templateBusinessPhone = Array.isArray(template.businessPhones)
? template.businessPhones[0]
: template.businessPhones
if (templateBusinessPhone) {
formControl.setValue('businessPhones', [templateBusinessPhone])
}
// Reset manual edit flags when template changes
setDisplayNameManuallySet(false)
setUsernameManuallySet(false)

// Handle licenses - need to match the format expected by CippFormLicenseSelector
if (template.licenses && Array.isArray(template.licenses)) {
setFieldIfEmpty('licenses', template.licenses)
// Apply a template value to a field. When the template has a value we set
// it; when it doesn't and this is a switch we clear the field (emptyValue)
// so the previous template's value doesn't linger.
const applyField = (fieldName, value, emptyValue = '') => {
const hasValue = Array.isArray(value)
? value.length > 0
: value !== undefined && value !== null && value !== ''
if (hasValue) {
formControl.setValue(fieldName, value, { shouldDirty: true })
} else if (isSwitch) {
formControl.setValue(fieldName, emptyValue, { shouldDirty: true })
}
}

// Handle groups from template
const templateGroups = template.addToGroups || template.groupMemberships
if (templateGroups) {
const rawGroups = Array.isArray(templateGroups) ? templateGroups : [templateGroups]
const groups = rawGroups.map((g) => {
if (g.label && g.value) return g
const groupType = g.groupTypes?.includes('Unified')
? 'Microsoft 365'
: g.mailEnabled && !g.groupTypes?.includes('Unified')
? g.securityEnabled
? 'Mail-Enabled Security'
: 'Distribution list'
: 'Security'
return {
label: g.displayName,
value: g.id,
addedFields: { groupType },
}
})
if (groups.length > 0) {
const currentGroups = watchedFields.AddToGroups
if (!currentGroups || (Array.isArray(currentGroups) && currentGroups.length === 0)) {
formControl.setValue('AddToGroups', groups, { shouldDirty: true })
}
}
// Primary domain - accept both object and string formats
const primDomainValue = template.primDomain
? typeof template.primDomain === 'string'
? { label: template.primDomain, value: template.primDomain }
: template.primDomain
: null
applyField('primDomain', primDomainValue, null)

// Usage location - accept both object and string formats
const usageLocationCode =
typeof template.usageLocation === 'string'
? template.usageLocation
: template.usageLocation?.value
const country = usageLocationCode
? countryList.find((c) => c.Code === usageLocationCode)
: null
applyField(
'usageLocation',
country ? { label: country.Name, value: country.Code } : null,
null
)

applyField('jobTitle', template.jobTitle)
applyField('streetAddress', template.streetAddress)
applyField('city', template.city)
applyField('state', template.state)
applyField('postalCode', template.postalCode)
applyField('country', template.country)
applyField('companyName', template.companyName)
applyField('department', template.department)
applyField('mobilePhone', template.mobilePhone)

const templateBusinessPhone = Array.isArray(template.businessPhones)
? template.businessPhones[0]
: template.businessPhones
applyField('businessPhones', templateBusinessPhone ? [templateBusinessPhone] : [], [])

// Licenses - match the format expected by CippFormLicenseSelector
applyField(
'licenses',
Array.isArray(template.licenses) ? template.licenses : [],
[]
)

// Groups from template
const templateGroups = template.addToGroups || template.groupMemberships
const rawGroups = templateGroups
? Array.isArray(templateGroups)
? templateGroups
: [templateGroups]
: []
const groups = rawGroups.map((g) => {
if (g.label && g.value) return g
const groupType = g.groupTypes?.includes('Unified')
? 'Microsoft 365'
: g.mailEnabled && !g.groupTypes?.includes('Unified')
? g.securityEnabled
? 'Mail-Enabled Security'
: 'Distribution list'
: 'Security'
return {
label: g.displayName,
value: g.id,
addedFields: { groupType },
}
})
applyField('AddToGroups', groups, [])

// Populate custom user attributes from template
if (template.defaultAttributes) {
Object.entries(template.defaultAttributes).forEach(([key, attr]) => {
if (attr?.Value) {
setFieldIfEmpty(`defaultAttributes.${key}.Value`, attr.Value)
}
// Custom user attributes. On a switch, clear every known attribute field
// first so attributes the new template doesn't define don't linger, then
// apply the template's values.
if (isSwitch) {
userSettingsDefaults?.userAttributes
?.filter((attribute) => attribute.value !== 'sponsor')
.forEach((attribute) => {
formControl.setValue(`defaultAttributes.${attribute.label}.Value`, '', {
shouldDirty: true,
})
})
}
}
if (template.defaultAttributes) {
Object.entries(template.defaultAttributes).forEach(([key, attr]) => {
applyField(`defaultAttributes.${key}.Value`, attr?.Value)
})
}
}, [watchedFields.userTemplate, formType])

Expand Down
2 changes: 1 addition & 1 deletion src/data/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -6531,7 +6531,7 @@
"label": "Conditional Access Template",
"cat": "Templates",
"multiple": true,
"disabledFeatures": { "report": true, "warn": true, "remediate": false },
"disabledFeatures": { "report": false, "warn": false, "remediate": false },
"impact": "High Impact",
"addedDate": "2023-12-30",
"tag": [
Expand Down
8 changes: 4 additions & 4 deletions src/layouts/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ export const nativeMenuItems = [
},
{
title: 'Device Management',
permissions: ['Endpoint.MEM.*'],
permissions: ['Endpoint.MEM.*', 'Endpoint.Device.*'],
items: [
{
title: 'Devices',
Expand Down Expand Up @@ -821,7 +821,7 @@ export const nativeMenuItems = [
},
{
title: 'Transport',
permissions: ['Exchange.TransportRule.*'],
permissions: ['Exchange.TransportRule.*', 'Exchange.Connector.*'],
items: [
{
title: 'Transport rules',
Expand Down Expand Up @@ -849,7 +849,7 @@ export const nativeMenuItems = [
},
{
title: 'Spamfilter',
permissions: ['Exchange.SpamFilter.*'],
permissions: ['Exchange.SpamFilter.*', 'Exchange.ConnectionFilter.*'],
items: [
{
title: 'Spamfilter',
Expand Down Expand Up @@ -882,7 +882,7 @@ export const nativeMenuItems = [
},
{
title: 'Resource Management',
permissions: ['Exchange.Equipment.*'],
permissions: ['Exchange.Equipment.*', 'Exchange.Room.*'],
items: [
{
title: 'Equipment',
Expand Down
2 changes: 1 addition & 1 deletion src/pages/teams-share/sharing-report/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ const Page = () => {
>
<Typography variant="body2" color="text.secondary">
{summary.lastDataRefresh
? `Last data refresh: ${new Date(summary.lastDataRefresh).toLocaleString()}`
? `Last data refresh: ${new Date(summary.lastDataRefresh.UtcDateTime).toLocaleString()}`
: ''}
</Typography>
<Button
Expand Down