diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..faf864265ea9 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,40 @@ +## Commit messages + +Generate all commit messages in **Conventional Commits** format: + +``` +(): + +[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 +``` diff --git a/.github/workflows/Conventional_Commits.yml b/.github/workflows/Conventional_Commits.yml index 6e026c8366ca..a5a669b02e55 100644 --- a/.github/workflows/Conventional_Commits.yml +++ b/.github/workflows/Conventional_Commits.yml @@ -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}"`); diff --git a/package.json b/package.json index 25cc8370c899..de4f7dcefcc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.6.0", + "version": "10.6.1", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index dc5c99678f74..a43d733ba6b2 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.6.0" + "version": "10.6.1" } diff --git a/src/components/CippComponents/CippAppTemplateDrawer.jsx b/src/components/CippComponents/CippAppTemplateDrawer.jsx index 715fd72047fa..e00873fbca28 100644 --- a/src/components/CippComponents/CippAppTemplateDrawer.jsx +++ b/src/components/CippComponents/CippAppTemplateDrawer.jsx @@ -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 @@ -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] } diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index a936c22b168c..538ba27063d4 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -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 @@ -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 } } }, [ @@ -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]) diff --git a/src/data/standards.json b/src/data/standards.json index 13b65e4926fd..2eee70c7420b 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -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": [ diff --git a/src/layouts/config.js b/src/layouts/config.js index 9958af388d55..789b3eb54c83 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -585,7 +585,7 @@ export const nativeMenuItems = [ }, { title: 'Device Management', - permissions: ['Endpoint.MEM.*'], + permissions: ['Endpoint.MEM.*', 'Endpoint.Device.*'], items: [ { title: 'Devices', @@ -821,7 +821,7 @@ export const nativeMenuItems = [ }, { title: 'Transport', - permissions: ['Exchange.TransportRule.*'], + permissions: ['Exchange.TransportRule.*', 'Exchange.Connector.*'], items: [ { title: 'Transport rules', @@ -849,7 +849,7 @@ export const nativeMenuItems = [ }, { title: 'Spamfilter', - permissions: ['Exchange.SpamFilter.*'], + permissions: ['Exchange.SpamFilter.*', 'Exchange.ConnectionFilter.*'], items: [ { title: 'Spamfilter', @@ -882,7 +882,7 @@ export const nativeMenuItems = [ }, { title: 'Resource Management', - permissions: ['Exchange.Equipment.*'], + permissions: ['Exchange.Equipment.*', 'Exchange.Room.*'], items: [ { title: 'Equipment', diff --git a/src/pages/teams-share/sharing-report/index.js b/src/pages/teams-share/sharing-report/index.js index 1a6da5aab857..e75076bfb5b1 100644 --- a/src/pages/teams-share/sharing-report/index.js +++ b/src/pages/teams-share/sharing-report/index.js @@ -248,7 +248,7 @@ const Page = () => { > {summary.lastDataRefresh - ? `Last data refresh: ${new Date(summary.lastDataRefresh).toLocaleString()}` + ? `Last data refresh: ${new Date(summary.lastDataRefresh.UtcDateTime).toLocaleString()}` : ''}