Save pathway shares

refs VICE-3932
flag=learner_passport
flag=learner_passport_r2

test plan:
  - edit a pathway
  - click on the pencil in the pathway box
  > expect any existing shares to be in the tray (at the bottom)
    (if you are editing the sample pathway, it will be
     the rolling stones)
  - edit the shares and save
  - re-edit the pathway
  > expect the changes
  - publish the pathway
  - edit the pathway
  > expect the changes

Change-Id: I79325ca663afa8ee24dcd585ab34a05394e93373
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/338475
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Aaron Suggs <aaron.suggs@instructure.com>
QA-Review: Aaron Suggs <aaron.suggs@instructure.com>
Product-Review: Aaron Suggs <aaron.suggs@instructure.com>
This commit is contained in:
Ed Schiebel 2024-01-23 10:19:45 -07:00 committed by Aaron Suggs
parent e0c7775bbf
commit 172f51818a
6 changed files with 110 additions and 31 deletions

View File

@ -391,6 +391,7 @@ class LearnerPassportController < ApplicationController
milestones: [],
completion_award: nil,
learner_groups: [],
shares: [],
}
end
@ -450,7 +451,36 @@ class LearnerPassportController < ApplicationController
learning_outcomes: [],
achievements_earned: [],
learner_groups: ["2", "3"],
shares: [],
shares: [
{
id: "rs1",
name: "Mick Jagger",
sortable_name: "Jagger, Mick",
avatar_url: "/images/messages/avatar-50.png",
role: "collaborator",
},
{
id: "rs2",
name: "Keith Richards",
sortable_name: "Richards, Keith",
avatar_url: "/images/messages/avatar-50.png",
role: "collaborator",
},
{
id: "rs3",
name: "Charlie Watts",
sortable_name: "Watts, Charlie",
avatar_url: "/images/messages/avatar-50.png",
role: "viewer",
},
{
id: "rs4",
name: "Ronnie Wood",
sortable_name: "Wood, Ronnie",
avatar_url: "/images/messages/avatar-50.png",
role: "reviewer",
},
],
}
end
@ -502,6 +532,10 @@ class LearnerPassportController < ApplicationController
"learner_passport_pathway_template #{@current_user.global_id}"
end
def pathway_sample_key
"learner_passport_pathway_sample #{@current_user.global_id}"
end
def index
js_env[:FEATURES][:learner_passport] = @domain_root_account.feature_enabled?(:learner_passport)
js_env[:FEATURES][:learner_passport_r2] = @domain_root_account.feature_enabled?(:learner_passport_r2)
@ -764,6 +798,29 @@ class LearnerPassportController < ApplicationController
render json: pathway
end
def pathway_share_users
search_term = params[:search_term] || ""
return render json: [{ message: "search term must be at least 2 characters long" }], status: :bad_request if search_term.blank? || search_term.length < 2
results = User.where("LOWER(name) LIKE ?", "%#{search_term.downcase}%")
.and(User.where(TeacherEnrollment.where("user_id=users.id").arel.exists).or(User.where(AccountUser.where("user_id=users.id").arel.exists)))
.order("sortable_name")
.limit(10)
.map { |u| { id: u.id, name: u.name, sortable_name: u.sortable_name, avatar_url: u.avatar_url, role: "viewer" } }
# results = UserSearch.for_user_in_context(search_term,
# Account.default,
# @current_user,
# session,
# {
# order: "asc",
# sort: "sortable_name",
# enrollment_type: "teacher_enrollment",
# include_deleted_users: false
# })
render json: results
end
def reset
if params.key? :empty
Rails.cache.write(current_portfolios_key, [], expires_in: CACHE_EXPIRATION)
@ -774,7 +831,7 @@ class LearnerPassportController < ApplicationController
Rails.cache.write(current_portfolios_key, [sample_portfolio.clone], expires_in: CACHE_EXPIRATION)
sample_project = Rails.cache.fetch(project_sample_key) { learner_passport_project_sample }
Rails.cache.write(current_projects_key, [sample_project.clone], expires_in: CACHE_EXPIRATION)
sample_pathway = Rails.cache.fetch(current_pathways_key) { learner_passport_pathway_sample }
sample_pathway = Rails.cache.fetch(pathway_sample_key) { learner_passport_pathway_sample }
Rails.cache.write(current_pathways_key, [sample_pathway.clone], expires_in: CACHE_EXPIRATION)
end
render json: { message: "Portfolios reset" }, status: :accepted

View File

@ -947,6 +947,7 @@ CanvasRails::Application.routes.draw do
put "passport/data/pathways/create" => "learner_passport#pathway_create"
post "passport/data/pathways/:pathway_id" => "learner_passport#pathway_update"
get "passport/data/pathways/show/:pathway_id" => "learner_passport#pathway_show"
get "passport/data/pathways/share_users" => "learner_passport#pathway_share_users"
get "passport/data/skills" => "learner_passport#skills_index"
get "passport/data/reset" => "learner_passport#reset"

View File

@ -178,7 +178,7 @@ const AddLearnerGroupsTray = ({
<View as="div" padding="small medium" borderWidth="small 0 0 0" textAlign="end">
<Button onClick={onClose}>Cancel</Button>
<Button margin="0 0 0 small" onClick={handleSave}>
Save Achievement
Save Groups
</Button>
</View>
</Flex.Item>

View File

@ -34,7 +34,7 @@ import type {
LearnerGroupType,
PathwayDetailData,
PathwayBadgeType,
CanvasUserSearchResultType,
PathwayUserShareType,
} from '../../../types'
import AddBadgeTray from './AddBadgeTray'
import AddLearnerGroupsTray, {LearnerGroupCard} from './AddLearnerGroupsTray'
@ -66,7 +66,7 @@ const PathwayDetailsTray = ({
const [selectedLearnerGroupIds, setSelectedLearnerGroupIds] = useState<string[]>(
pathway.learner_groups
)
const [selectedShares, setSelectedShares] = useState<CanvasUserSearchResultType[]>([])
const [selectedShares, setSelectedShares] = useState<PathwayUserShareType[]>(pathway.shares)
const [addBadgeTrayOpenKey, setAddBadgeTrayOpenKey] = useState(0)
const [addLearnerGroupsTrayOpenKey, setAddLearnerGroupsTrayOpenKey] = useState(0)
@ -82,8 +82,22 @@ const PathwayDetailsTray = ({
const handleSave = useCallback(() => {
if (!title) return
const badge = allBadges.find(b => b.id === currSelectedBadgeId)
onSave({title, description, completion_award: badge, learner_groups: selectedLearnerGroupIds})
}, [allBadges, currSelectedBadgeId, description, onSave, selectedLearnerGroupIds, title])
onSave({
title,
description,
completion_award: badge,
learner_groups: selectedLearnerGroupIds,
shares: selectedShares,
})
}, [
allBadges,
currSelectedBadgeId,
description,
onSave,
selectedLearnerGroupIds,
selectedShares,
title,
])
const handleTitleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>, newTitle: string) => {
@ -115,7 +129,7 @@ const PathwayDetailsTray = ({
setAddLearnerGroupsTrayOpenKey(0)
}, [])
const handleChangeSharedUser = useCallback((users: CanvasUserSearchResultType[]) => {
const handleChangeSharedUser = useCallback((users: PathwayUserShareType[]) => {
setSelectedShares(users)
}, [])

View File

@ -29,23 +29,21 @@ import {Spinner} from '@instructure/ui-spinner'
import {Table} from '@instructure/ui-table'
import {View} from '@instructure/ui-view'
import useFetchApi from '@canvas/use-fetch-api-hook'
// import useDebouncedSearchTerm from '@canvas/search-item-selector/react/hooks/useDebouncedSearchTerm'
import type {CanvasUserSearchResultType} from '../../../../types'
import type {CanvasUserSearchResultType, PathwayUserShareType} from '../../../../types'
const MIN_SEARCH_TERM_LENGTH = 2
const SEARCH_DEBOUNCE_MS = 750
type CanvasUserFinderProps = {
selectedUsers: CanvasUserSearchResultType[]
onChange: (newSelectedUsers: CanvasUserSearchResultType[]) => void
selectedUsers: PathwayUserShareType[]
onChange: (newSelectedUsers: PathwayUserShareType[]) => void
}
const CanvasUserFinder = ({selectedUsers, onChange}: CanvasUserFinderProps) => {
const [searchTerm, setSearchTerm] = useState<string>('')
const [debouncedSearchterm, setDebouncedSearchTerm] = useState<string>('_') // will cause a 400 on the first fetch
const [searchResults, setSearchResults] = useState<CanvasUserSearchResultType[] | null>(null)
const [currSelectedUsers, setCurrSelectedUsers] =
useState<CanvasUserSearchResultType[]>(selectedUsers)
const [searchResults, setSearchResults] = useState<PathwayUserShareType[] | null>(null)
const [currSelectedUsers, setCurrSelectedUsers] = useState<PathwayUserShareType[]>(selectedUsers)
const [inFlight, setInFlight] = useState<boolean>(false)
const [showSearchResults, setShowSearchResults] = useState<boolean>(false)
@ -59,26 +57,30 @@ const CanvasUserFinder = ({selectedUsers, onChange}: CanvasUserFinderProps) => {
// the controller will return immediately a 400 since debouncedSearchterm is empty
useFetchApi(
{
path: `/api/v1/accounts/${ENV.ACCOUNT_ID}/users`,
path: `/users/${ENV.current_user.id}/passport/data/pathways/share_users`, // `/api/v1/accounts/${ENV.ACCOUNT_ID}/users`,
params: {
search_term: debouncedSearchterm,
},
success: useCallback((results: CanvasUserSearchResultType[]) => {
if (results === undefined) {
// there is no way to keep useFetchApi from firing when dependencies
// aren't adequate for the search. This is probably the 400 response
// because the search_term is too short
setSearchResults(null)
} else {
setSearchResults(results || [])
if (results.length > 0) setShowSearchResults(true)
const shares: PathwayUserShareType[] = results.map(r => {
return {
id: r.id,
name: r.name,
role: 'viewer',
sortable_name: r.sortable_name,
avatar_url: r.avatar_url,
}
})
// strip unwanted fields
setSearchResults(shares || [])
setShowSearchResults(true)
}
}, []),
params: {
sort: 'sortable_name',
order: 'asc',
'include[]': 'avatar_url',
search_term: debouncedSearchterm,
per_page: 20,
},
error: useCallback(() => {
// this will happen on the first fetch, since debouncedSearchterm < 2 chars long
setSearchResults(null)
}, []),
loading: isLoading,

View File

@ -193,14 +193,19 @@ export interface RequirementData {
canvas_content?: CanvasRequirementSearchResultType
}
export type CanvasUserSearchResultType = {
export interface CanvasUserSearchResultType {
id: string
name: string
role?: string
sortable_name: string
avatar_url: string
}
export type PathwayUserShareRoleType = 'collaborator' | 'reviewer' | 'viewer'
export interface PathwayUserShareType extends CanvasUserSearchResultType {
role: PathwayUserShareRoleType
}
// this is a node in the pathway tree
export interface MilestoneData {
id: string
@ -230,7 +235,7 @@ export interface PathwayDetailData extends PathwayData {
learning_outcomes: SkillData[]
completion_award: PathwayBadgeType | null
learner_groups: string[] // learner group ids
shares: CanvasUserSearchResultType[]
shares: PathwayUserShareType[]
first_milestones: string[] // ids of the milestone children of the root pathway
milestones: MilestoneData[] // all the milestones in the pathway
}