Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -5341,6 +5341,18 @@
#Default: false
#scorm.config.showNavBar.default=true

# ###############################
# MICROSOFT TEAMS
# ###############################

#SAK-52355 Enable forced synchronization in Microsoft Teams
#DEFAULT: none, use "all" for all sites or specify a list of site types to allow. Comma-separated list of sitetype: course,project,...
#microsoft.forced.synchronization.sitetype=all

#SAK-52355 Enable forced synchronization in Microsoft Teams
#DEFAULT: microsoft.synchronized=true, use your own name and value property. IMPORTANT: The administrator should add this name and optional value in the Microsoft Admin Tool settings, in the corresponding "Site property" input.
#microsoft.forced.synchronization.propertynameandoptionalvalue=microsoft.synchronized=true
Comment on lines +5352 to +5354
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Correct the documented default for forced site-property mapping.

Line 5353 currently states a default of microsoft.synchronized=true, but runtime behavior treats this setting as optional/unset by default (no property is applied when blank). This can mislead admins during configuration.

🛠️ Proposed doc-only fix
-#DEFAULT: microsoft.synchronized=true, use your own name and value property. IMPORTANT: The administrator should add this name and optional value in the Microsoft Admin Tool settings, in the corresponding "Site property" input.
-#microsoft.forced.synchronization.propertynameandoptionalvalue=microsoft.synchronized=true
+#DEFAULT: none (no forced site property is applied unless explicitly configured)
+#EXAMPLE: microsoft.synchronized=true
+#IMPORTANT: The administrator should add this name and optional value in the Microsoft Admin Tool settings, in the corresponding "Site property" input.
+#microsoft.forced.synchronization.propertynameandoptionalvalue=microsoft.synchronized=true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@config/configuration/bundles/src/bundle/org/sakaiproject/config/bundle/default.sakai.properties`
around lines 5352 - 5354, The documentation incorrectly states a default of
"microsoft.synchronized=true"; update the comment for the property key
microsoft.forced.synchronization.propertynameandoptionalvalue to indicate that
there is no default (the setting is unset/blank by default and no site-property
is applied) so administrators are not misled—replace the stated default with a
clear note such as "DEFAULT: none (setting is blank/unset by default; no
property is applied)" and keep the existing guidance about adding a name/value
in the Microsoft Admin Tool.


# ###############################
# Content-Review
# ###############################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@
}
#toolHolderWW {
@include display-flex;
.mathJaxToggleArea, .subPageNavToggleArea {
.mathJaxToggleArea, .subPageNavToggleArea, .microsoftSynchronizationArea {
@include display-flex;
@include flex-direction(row-reverse);
@include justify-content(flex-end); // align to the left
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ public String autoConfig(

if(!autoConfigSessionBean.isRunning()) {

//get all synchronizations
List<SiteSynchronization> ssList = microsoftSynchronizationService.getAllSiteSynchronizations(false);
//get all enabled synchronizations
List<SiteSynchronization> ssList = microsoftSynchronizationService.getAllEnabledSiteSynchronizations(false);

Comment on lines +121 to 123
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Auto-config should exclude existing synchronizations regardless of disabled state.

This code now filters out only enabled synchronizations before building candidate site/team maps. Disabled synchronizations still exist in persistence, so they can be re-proposed and create duplicate/conflicting relationships.

🛠️ Suggested direction
-List<SiteSynchronization> ssList = microsoftSynchronizationService.getAllEnabledSiteSynchronizations(false);
+// Use a method that returns all persisted synchronizations (enabled + disabled)
+List<SiteSynchronization> ssList = microsoftSynchronizationService.getAllSiteSynchronizations(false);

If getAllSiteSynchronizations(false) no longer exists, add an explicit “includeDisabled” API path and use it here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/AutoConfigController.java`
around lines 121 - 123, The AutoConfigController currently calls
microsoftSynchronizationService.getAllEnabledSiteSynchronizations(false), which
only returns enabled synchronizations and allows disabled records to be
re-proposed; change the call to retrieve all synchronizations including disabled
(e.g., use a new or existing method like getAllSiteSynchronizations(true) or add
an includeDisabled parameter to microsoftSynchronizationService and call that)
so the construction of candidate site/team maps excludes any existing
synchronization regardless of disabled state; update usages in
AutoConfigController (the ssList variable and any downstream logic that expects
only enabled records) to handle the full set accordingly.

//get (filtered) sites
List<Site> sitesList = sakaiProxy.getSakaiSites(requestBody.getFilter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,24 @@ public AjaxResponse updateSiteSynchronizationForced(@PathVariable String id, @Re

return ret;
}


//called by AJAX - returns JSON
@PostMapping(path = {"/setDisabled-siteSynchronization/{id}"}, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public AjaxResponse updateSiteSynchronizationDisabled(@PathVariable String id, @RequestParam Boolean disabled, Model model) {
SiteSynchronization ss = microsoftSynchronizationService.getSiteSynchronization(SiteSynchronization.builder().id(id).build());
AjaxResponse ret = new AjaxResponse();
ret.setStatus(false);
ret.setError(rb.getString("error.changing_synchronization_status"));
if(ss != null) {
ss.setDisabled(disabled);
microsoftSynchronizationService.saveOrUpdateSiteSynchronization(ss);
ret.setStatus(true);
ret.setError("");
}
return ret;
}

//called by AJAX - returns JSON
@GetMapping(path = {"/setDate-siteSynchronization/{id}"}, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ index.run.disabled=Run Disabled
index.refresh=Refresh
index.groups=Groups
index.edit_groups=Edit Groups
index.enable=Enable synchronization
index.disable=Disable synchronization
index.selected_id=Selected id
index.delete_selected=Delete Selected
index.remove_team_users=Remove Team Users
Expand Down Expand Up @@ -221,6 +223,7 @@ error.new_channel_empty=New channel name can not be empty
error.new_channel_with_same_name= New channel name can not have the same name of one of the channels created
error.delete_group_synchronization=Error removing selected Group Synchronization
error.set_forced_synchronization=Error setting forced
error.changing_synchronization_status=Error changing the synchronization status
error.deleting_site_synchronizations=Error removing one or more selected synchronizations
error.cleaning_team=Error cleaning one or more selected synchronizations
error.set_dates=Error setting dates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ index.run.disabled=Ejecutar Deshabilitado
index.refresh=Refrescar
index.groups=Grupos
index.edit_groups=Editar Grupos
index.enable=Habilitar sincronizaci\u00f3n
index.disable=Deshabilitar sincronizaci\u00f3n
index.selected_id=Id seleccionado
index.delete_selected=Borrar seleccionado
index.remove_team_users=Eliminar Usuarios de Teams
Expand Down Expand Up @@ -220,6 +222,7 @@ error.new_channel_empty=El nombre del nuevo canal no puede estar vac\u00edo
error.new_channel_with_same_name=El nombre del nuevo canal no puede tener el mismo nombre que uno de los canales ya creados
error.delete_group_synchronization=Error eliminando la sincronizaci\u00f3n de grupo seleccionada
error.set_forced_synchronization=Error de configuraci\u00f3n forzado
error.changing_synchronization_status=Error al cambiar el estado de la sincronizaci\u00f3n
error.deleting_site_synchronizations=Error al eliminar una o varias sincronizaciones seleccionadas
error.cleaning_team=Error al limpiar una o m\u00e1s sincronizaciones seleccionadas
error.set_dates=Error al establecer las fechas
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@
<a th:href="@{/editGroupSynchronization/{id}(id=${row.id})}" th:title="#{index.edit_groups}" th:classappend="${teamsMap[row.teamId]} == null ? 'hidden'" >
<i class="fa fa-users"></i>
</a>
<!-- Disable/Enable -->
<a href="#" onclick="toggleDisabled(this); return false;" th:title="${row.disabled} ? #{index.enable} : #{index.disable}"
th:aria-label="${row.disabled} ? #{index.enable} : #{index.disable}">
<i th:class="${row.disabled} ? 'fa fa-toggle-off' : 'fa fa-toggle-on'"></i>
<span class="sr-only" th:text="${row.disabled} ? #{index.enable} : #{index.disable}"></span>
</a>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
<div class="col-lg-1 multi-button" th:if="${row.teamId == null || row.teamId == ''}">
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,35 @@ <h1 th:text="#{index_title}"></h1>
}
}

async function toggleDisabled(elem) {
let baseURL = "[(@{/setDisabled-siteSynchronization/})]";
let rowId = elem.closest('.table-row').id.replace('row_', '');
let icon = elem.querySelector('i');
let isDisabled = icon.classList.contains('fa-toggle-off');

let url = baseURL + rowId + "?disabled=" + !isDisabled;

let response = await fetch(url, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
let data = await response.json();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (data.status == false) {
showAjaxError(data.error);
} else {
if (!isDisabled) {
icon.classList.replace('fa-toggle-on', 'fa-toggle-off');
elem.title = /*[[#{index.enable}]]*/ '';
} else {
icon.classList.replace('fa-toggle-off', 'fa-toggle-on');
elem.title = /*[[#{index.disable}]]*/ '';
}
Comment on lines +135 to +141
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Update the control’s accessible name when toggle state changes.

On success, only title is updated. aria-label (and the hidden label text) stay stale, so assistive tech can announce the wrong action after toggling.

♿ Proposed fix
 				if (data.status == false) {
 					showAjaxError(data.error);
 				} else {
+					let nextLabel;
 					if (!isDisabled) {
 						icon.classList.replace('fa-toggle-on', 'fa-toggle-off');
-						elem.title = /*[[#{index.enable}]]*/ '';
+						nextLabel = /*[[#{index.enable}]]*/ '';
 					} else {
 						icon.classList.replace('fa-toggle-off', 'fa-toggle-on');
-						elem.title = /*[[#{index.disable}]]*/ '';
+						nextLabel = /*[[#{index.disable}]]*/ '';
 					}
+					elem.title = nextLabel;
+					elem.setAttribute('aria-label', nextLabel);
+					const srText = elem.querySelector('.sr-only, .visually-hidden');
+					if (srText) srText.textContent = nextLabel;
 				}

As per coding guidelines, **/*.{html,xhtml,vm,js,ts}: Follow accessibility best practices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/index.html`
around lines 135 - 141, The toggle handler only updates elem.title and the icon
classes when isDisabled changes, leaving the element's accessible name stale;
update the element's aria-label in the same branches (and also update any
associated hidden label text if present) so assistive tech reflects the new
action. In the branches that call icon.classList.replace(...) and set
elem.title, also set elem.setAttribute('aria-label', /*[[#{index.enable}]]*/ or
/*[[#{index.disable}]]*/ as appropriate and update the hidden label node (e.g.,
a sibling with a screen-reader-only class) to the same localized string so the
visible title, aria-label, and hidden label remain consistent with isDisabled.

}
}

var dateTimeout = null;
async function changeDate(input) {
if(dateTimeout != null){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public static enum PermissionRoles { READ, WRITE }
void createTeamFromGroupAsync(String groupId) throws MicrosoftCredentialsException;

boolean deleteTeam(String teamId) throws MicrosoftCredentialsException;
boolean archiveTeam(String teamId) throws MicrosoftCredentialsException;
boolean unarchiveTeam(String teamId) throws MicrosoftCredentialsException;

MicrosoftMembersCollection getTeamMembers(String id, MicrosoftUserIdentifier key) throws MicrosoftCredentialsException;
MicrosoftUser checkUserInTeam(String identifier, String teamId, MicrosoftUserIdentifier key) throws MicrosoftCredentialsException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
public interface MicrosoftSynchronizationService {

// ------------ Site Synchronization ---------------------------
List<SiteSynchronization> getAllSiteSynchronizations(boolean fillSite);
List<SiteSynchronization> getAllEnabledSiteSynchronizations(boolean fillSite);
List<SiteSynchronization> getFilteredSiteSynchronizations(boolean fillSite, SakaiSiteFilter filter, ZonedDateTime fromDate, ZonedDateTime toDate);

SiteSynchronization getSiteSynchronization(SiteSynchronization ss);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ public class SiteSynchronization {
@Convert(converter = JpaConverterSynchronizationStatus.class)
private SynchronizationStatus status = SynchronizationStatus.NONE;

@Column(name = "forced")
private boolean forced;
@Column(name = "forced", nullable = false)
@Builder.Default
private boolean forced = false;

@Column(name = "date_from")
private ZonedDateTime syncDateFrom;
Expand All @@ -86,6 +87,10 @@ public class SiteSynchronization {
@Column(name = "status_updated_at")
private ZonedDateTime statusUpdatedAt;

@Column(name = "disabled", nullable = false)
@Builder.Default
private boolean disabled = false;
Comment thread
jesusmmp marked this conversation as resolved.

@OneToMany(mappedBy = "siteSynchronization", fetch = FetchType.LAZY, orphanRemoval = true)
@Cascade(CascadeType.ALL)
@OnDelete(action = OnDeleteAction.CASCADE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.sakaiproject.serialization.SerializableRepository;

public interface MicrosoftSiteSynchronizationRepository extends SerializableRepository<SiteSynchronization, String> {
List<SiteSynchronization> findAllEnabled();
Optional<SiteSynchronization> findById(String id);
Optional<SiteSynchronization> findBySiteTeam(String siteId, String teamId);
List<SiteSynchronization> findBySite(String siteId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.google.gson.Gson;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.graph.requests.GraphServiceClient;
import com.microsoft.graph.requests.UserCollectionPage;
import com.microsoft.graph.requests.GroupCollectionPage;
Expand Down Expand Up @@ -147,7 +148,9 @@
import com.microsoft.graph.models.OnlineMeetingRole;
import com.microsoft.graph.models.Permission;
import com.microsoft.graph.models.PermissionGrantParameterSet;
import com.microsoft.graph.models.Site;
import com.microsoft.graph.models.Team;
import com.microsoft.graph.models.TeamArchiveParameterSet;
import com.microsoft.graph.models.TeamVisibilityType;
import com.microsoft.graph.models.ThumbnailSet;
import com.microsoft.graph.models.UploadSession;
Expand Down Expand Up @@ -1010,6 +1013,87 @@ public boolean deleteTeam(String teamId) throws MicrosoftCredentialsException {
return false;
}

public boolean archiveTeam(String teamId) throws MicrosoftCredentialsException {
try {
// 1. Archive team without shouldSetSpoSiteReadOnlyForMembers (no supported in app-only)
TeamArchiveParameterSet requestBody = TeamArchiveParameterSet
.newBuilder()
.withShouldSetSpoSiteReadOnlyForMembers(false)
.build();

getGraphClient().teams(teamId)
.archive(requestBody)
.buildRequest()
.post();

// 2. Obtain the associated SharePoint site to ensure the team is fully archived before setting it to read-only
Site site = getGraphClient().groups(teamId)
.sites("root")
.buildRequest()
.get();

if (site == null || site.id == null) {
log.error("Could not retrieve SharePoint site for team: {}, site will not be set to read-only", teamId);
return false;
}

// 3. Set SharePoint site to read-only (as a backup in case the archive operation did not set it correctly)
JsonObject jsonBody = new JsonObject();
jsonBody.addProperty("lockState", "readOnly");

getGraphClient().customRequest("/sites/" + site.id)
.buildRequest()
.patch(jsonBody);

log.info("Team archived and SharePoint site set to read-only: teamId={}", teamId);

return true;
} catch(MicrosoftCredentialsException e) {
throw e;
} catch(Exception ex){
log.error("Error archiving Microsoft team: id={}", teamId, ex);
}
return false;
}

public boolean unarchiveTeam(String teamId) throws MicrosoftCredentialsException {
try {
//1. Unarchive team
getGraphClient().teams(teamId)
.unarchive()
.buildRequest()
.post();

// 2. Obtain the associated SharePoint site to ensure the team is fully unarchived before returning
Site site = getGraphClient().groups(teamId)
.sites("root")
.buildRequest()
.get();

if (site == null || site.id == null) {
log.error("Could not retrieve SharePoint site for team: {}, site will remain read-only", teamId);
return false;
}

//3. Set SharePoint site to unlocked (as a backup in case the unarchive operation did not set it correctly)
JsonObject jsonBody = new JsonObject();
jsonBody.addProperty("lockState", "unlocked");

getGraphClient().customRequest("/sites/" + site.id)
.buildRequest()
.patch(jsonBody);

log.info("Team unarchived and SharePoint site unlocked: teamId={}", teamId);

return true;
} catch (MicrosoftCredentialsException e) {
throw e;
} catch (Exception ex) {
log.error("Error unarchiving Microsoft team: id={}", teamId, ex);
}
return false;
}
Comment on lines +1016 to +1095
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find archive/unarchive call sites and inspect whether return values are consumed
rg -nP --type=java '\b(?:archiveTeam|unarchiveTeam)\s*\(' -C3

# Find standalone invocation statements (likely ignored return value)
ast-grep --pattern '$OBJ.archiveTeam($ARG);'
ast-grep --pattern '$OBJ.unarchiveTeam($ARG);'

Repository: sakaiproject/sakai

Length of output: 6413


Return values from archiveTeam() and unarchiveTeam() are ignored, allowing silent failures to proceed as success.

Callers in MicrosoftSynchronizationEnabler.java (lines 163 and 242) invoke these methods without checking the returned boolean. The methods return false on generic exceptions, but both call sites continue to log success and mutate synchronization state regardless of whether the archive/unarchive operation actually succeeded. This creates a correctness risk where Teams archive/unarchive failures are masked, leaving the system in an inconsistent state (disabled/enabled synchronization properties no longer match actual Team status).

Either check the return value and handle failure (if (!archiveTeam(...)) ...), or redesign these methods to fail loudly with exceptions so failures cannot be ignored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java`
around lines 1013 - 1041, archiveTeam(...) and unarchiveTeam(...) currently
swallow generic exceptions and return false, which callers in
MicrosoftSynchronizationEnabler.java ignore; change the behavior to fail loudly:
remove the broad catch(Exception) blocks in
MicrosoftCommonServiceImpl.archiveTeam and .unarchiveTeam so failures propagate
(keep rethrowing MicrosoftCredentialsException), and change the method
signatures to void (or keep boolean but throw an explicit
RuntimeException/MicrosoftOperationException on error) so callers in
MicrosoftSynchronizationEnabler.java (the call sites at the previously
referenced locations) must handle exceptions or check results; update those
callers to catch and handle the propagated exception (or check the returned
value) and only mutate synchronization state when the archive/unarchive
completes successfully.


@Override
public MicrosoftMembersCollection getTeamMembers(String id, MicrosoftUserIdentifier key) throws MicrosoftCredentialsException {
MicrosoftMembersCollection ret = new MicrosoftMembersCollection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ private boolean checkTeam(String teamId) throws MicrosoftCredentialsException {
}

@Override
public List<SiteSynchronization> getAllSiteSynchronizations(boolean fillSite) {
List<SiteSynchronization> result = StreamSupport.stream(microsoftSiteSynchronizationRepository.findAll().spliterator(), false)
public List<SiteSynchronization> getAllEnabledSiteSynchronizations(boolean fillSite) {
List<SiteSynchronization> result = StreamSupport.stream(microsoftSiteSynchronizationRepository.findAllEnabled().spliterator(), false)
.map(ss -> {
if(fillSite) {
ss.setSite(sakaiProxy.getSite(ss.getSiteId()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException {
session.setAttribute("origin", MicrosoftLogInvokers.JOB.getCode());
SakaiSiteFilter siteFilter = microsoftConfigurationService.getJobSiteFilter();

List<SiteSynchronization> list = microsoftSynchronizationService.getAllSiteSynchronizations(true);
List<SiteSynchronization> list = microsoftSynchronizationService.getAllEnabledSiteSynchronizations(true);
for(SiteSynchronization ss : list) {
int retryCount = 0;
while (retryCount < MAX_RETRIES) {
Expand All @@ -91,7 +91,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException {
}

if(ss.getSite() == null || !siteFilter.match(ss.getSite())) {
log.debug("Site with id={} skipped due to filter restrinctions", ss.getSiteId());
log.debug("Site with id={} skipped due to filter restrictions", ss.getSiteId());
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public List<SiteSynchronization> findAll(){
.list();
}

@Override
public List<SiteSynchronization> findAllEnabled() {
return (List<SiteSynchronization>) startCriteriaQuery()
.addOrder(Order.asc("status"))
.add(Restrictions.eq("disabled", false))
.list();
}

@Override
public Optional<SiteSynchronization> findById(String id) {
SiteSynchronization siteSynchronization = (SiteSynchronization) startCriteriaQuery().add(Restrictions.eq("id", id)).uniqueResult();
Expand Down
4 changes: 4 additions & 0 deletions site-manage/site-manage-tool/tool/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
<groupId>org.sakaiproject.scheduler</groupId>
<artifactId>scheduler-api</artifactId>
</dependency>
<dependency>
<groupId>org.sakaiproject.microsoft</groupId>
<artifactId>microsoft-api</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1152,3 +1152,9 @@ sinfo.gradebookgroupvnav.site=A single site instance
sinfo.gradebookgroupvnav.group=A different instance for the selected group(s)
sinfo.gradebookgroupvnav.selectGroups=Select one or more groups
sinfo.gradebookgroupvnav.noGroupsPresent=Note - There are currently no groups present in this site. Create groups in Site Info to enable additional options.

# SAK-52355
sinfo.synchronization.microsoft.name=Synchronization with Microsoft Teams
sinfo.synchronization.microsoft.allowForSite=Enable <strong>forced synchronization with Microsoft Teams</strong> for this site.
sinfo.synchronization.microsoft.confirmEnabled=You have enabled forced synchronization with Microsoft Teams for this site.
sinfo.synchronization.microsoft.enabled=Enabled for the site
Original file line number Diff line number Diff line change
Expand Up @@ -1124,3 +1124,9 @@ sinfo.gradebookgroupvnav.site=Una instancia de sitio
sinfo.gradebookgroupvnav.group=Una instancia diferente para cada grupo seleccionado
sinfo.gradebookgroupvnav.selectGroups=Nota - Debes seleccionar al menos un grupo
sinfo.gradebookgroupvnav.noGroupsPresent=Nota - Actualmente no hay grupos en este sitio. Debe crear grupos antes de poder asignar la tarea a un grupo concreto.

# SAK-52355
sinfo.synchronization.microsoft.name=Sincronizaci\u00f3n con Microsoft Teams
sinfo.synchronization.microsoft.allowForSite=Habilitar <strong>la sincronizaci\u00f3n forzada con Microsoft Teams</strong> para este sitio.
sinfo.synchronization.microsoft.confirmEnabled=Has habilitado la sincronizaci\u00f3n forzada con Microsoft Teams para este sitio.
sinfo.synchronization.microsoft.enabled=Activado para este sitio
Loading
Loading