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:
parent
e0c7775bbf
commit
172f51818a
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}, [])
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue