Skip to content
Merged
4 changes: 4 additions & 0 deletions src/components/repos/RepoCreate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import RepoCreateLanguages from './RepoCreateLanguages'
import RepoCreateAdditionalMetadata from './RepoCreateAdditionalMetadata'
import RepoCreatePublisher from './RepoCreatePublisher'
import RepoCreateHierarchy from './RepoCreateHierarchy'
import RepoCreateDisplay from './RepoCreateDisplay'
import RepoCreateIDAutoAssignment from './RepoCreateIDAutoAssignment'
import { parseRepoURL } from './utils'

Expand Down Expand Up @@ -544,6 +545,9 @@ const RepoCreate = () => {
<RepoCreateHierarchy sourceURL={isEdit ? repo?.url : ''} onChange={onChange} hierarchyRootURL={model.hierarchyRootURL} hierarchyMeaning={model.hierarchyMeaning} />
</FormSection>
}
<FormSection sx={{marginTop: '16px'}}>
<RepoCreateDisplay isSource={Boolean(selectedTab.hierarchy)} meta={model.meta} hierarchyRootURL={model.hierarchyRootURL} onChange={onChange} />
</FormSection>
{
selectedTab.autoAssignIDs &&
<FormSection sx={{marginTop: '16px'}}>
Expand Down
76 changes: 76 additions & 0 deletions src/components/repos/RepoCreateDisplay.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import Typography from '@mui/material/Typography'
import Select from '@mui/material/Select'
import MenuItem from '@mui/material/MenuItem'
import InputLabel from '@mui/material/InputLabel'
import FormControl from '@mui/material/FormControl'
import FormHelperText from '@mui/material/FormHelperText'
import ListItemText from '@mui/material/ListItemText'
import { isBlankJSONString, jsonToString, stringToJSON } from '../common/JSONTextField'

const RepoCreateDisplay = ({ isSource, meta, hierarchyRootURL, onChange }) => {
const { t } = useTranslation()
const parsed = isBlankJSONString(meta) ? {} : stringToJSON(meta)
const usable = Boolean(parsed && typeof parsed === 'object' && !Array.isArray(parsed))
const options = isSource ? ['table', 'card', 'hierarchy'] : ['table', 'card']
const savedView = parsed?.display?.default_view
const value = options.includes(savedView) ? savedView : 'table'
const defaultFilters = Object.entries(parsed?.display?.default_filter || {})
const helperText = !usable ? 'repo.default_view_invalid_meta' :
value === 'hierarchy' && !hierarchyRootURL ? 'repo.default_view_needs_root' : 'repo.default_view_help'
const onSelect = event => onChange('meta', jsonToString({...parsed, display: {...(parsed.display || {}), default_view: event.target.value}}))

return (
<div className='col-xs-12 padding-0'>
<Typography sx={{fontSize: '16px', fontWeight: 'bold'}}>
{t('repo.display')}
</Typography>
<Typography sx={{fontSize: '14px', color: 'secondary.40', marginTop: '8px'}}>
{t('repo.display_description')}
</Typography>
<div className='col-xs-12 padding-0' style={{marginTop: '24px'}}>
<FormControl variant='outlined' fullWidth disabled={!usable}>
<InputLabel id='default-view-label'>{t('repo.default_view')}</InputLabel>
<Select labelId='default-view-label' label={t('repo.default_view')} value={value} onChange={onSelect}>
{
options.map(option => (
<MenuItem key={option} value={option} disabled={option === 'hierarchy' && !hierarchyRootURL}>
<ListItemText
primary={t(`search.${option}`)}
secondary={<span style={{fontSize: '12px', whiteSpace: 'normal', display: 'block'}}>{t(`repo.default_view_${option}_description`)}</span>}
/>
</MenuItem>
))
}
</Select>
<FormHelperText>{t(helperText)}</FormHelperText>
</FormControl>
</div>
{
isSource &&
<div className='col-xs-12 padding-0' style={{marginTop: '24px'}}>
<Typography sx={{fontSize: '14px', fontWeight: 'bold', color: 'secondary.40'}}>
{t('repo.default_filters')}
</Typography>
{
defaultFilters.length ? defaultFilters.map(([key, value]) => (
<Typography key={key} sx={{fontSize: '14px'}}>
{key.replace(/^properties__/, '')}{String(value).startsWith('!') ? ` ≠ ${String(value).slice(1)}` : `: ${value}`}
</Typography>
)) : (
<Typography sx={{fontSize: '14px', color: 'secondary.40'}}>
{t('repo.default_filters_none')}
</Typography>
)
}
<Typography sx={{fontSize: '14px', color: 'secondary.40', marginTop: '8px'}}>
{t('repo.default_filters_help')}
</Typography>
</div>
}
</div>
)
}

export default RepoCreateDisplay
9 changes: 5 additions & 4 deletions src/components/repos/RepoHome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const RepoHome = () => {
const [tabs, setTabs] = React.useState(getRepoTabs)

const [status, setStatus] = React.useState(false)
const [repo, setRepo] = React.useState(false)
const [{repo, repoRoute}, setRepoState] = React.useState({repo: false, repoRoute: null})
const [owner, setOwner] = React.useState(false)
const [repoSummary, setRepoSummary] = React.useState(false)
const [versions, setVersions] = React.useState(false)
Expand Down Expand Up @@ -161,7 +161,7 @@ const RepoHome = () => {

setStatus(newStatus)
setLoading(false)
setRepo(_repo)
setRepoState({repo: _repo, repoRoute: route})
if(!isCollection)
setContextRepo(_repo)
if(!sameRepoAsBefore)
Expand Down Expand Up @@ -510,7 +510,7 @@ const RepoHome = () => {
return
let meta = {...repo?.meta, display: {...repo?.meta?.display, default_filter: appliedFilters}}
APIService.new().overrideURL(repo.version_url || repo.url).patch({meta: meta}).then(() => {
setRepo({...repo, meta: meta})
setRepoState(prev => prev.repo?.version_url === repo.version_url ? {...prev, repo: {...prev.repo, meta: meta}} : prev)
setAlert({severity: 'success', message: t('common.success_update')})
})
}
Expand Down Expand Up @@ -538,7 +538,8 @@ const RepoHome = () => {
const onVersionEditClick = () => isVersion && setVersionForm({edit: true, version: repo, expansions: []})
const onReleaseVersionClick = () => isVersion && setReleaseTarget(repo)
// References are collection-only, so never query them on a source.
const _canRenderSearch = repo?.id && ['concepts', 'mappings', 'references'].includes(tab) && (tab !== 'references' || isCollection) && canRenderSearch
// Search reads the repo's saved defaults when it mounts, so wait for the version the route asks for
const _canRenderSearch = repo?.id && ['concepts', 'mappings', 'references'].includes(tab) && (tab !== 'references' || isCollection) && canRenderSearch && isRepoForRoute && isSameRepoScope(repoRoute, route)
const showProcessingBanner = _canRenderSearch && isVersionProcessing(currentRepo)
const heightTakenInProcessingBanner = showProcessingBanner ? 43 : 0
return (
Expand Down
49 changes: 39 additions & 10 deletions src/components/search/Search.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import UserIcon from '@mui/icons-material/Person';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import TransformIcon from '@mui/icons-material/Transform';
import DownIcon from '@mui/icons-material/KeyboardArrowDown';
import { forEach, keys, pickBy, isEmpty, find, uniq, has, orderBy as sortBy, uniqBy, omit, max, isEqual, isBoolean } from 'lodash';
import { forEach, keys, pick, pickBy, isEmpty, find, uniq, has, orderBy as sortBy, uniqBy, omit, max, isEqual, isBoolean } from 'lodash';
import { COLORS } from '../../common/colors';
import { dropVersion, highlightTexts, isLoggedIn, getCurrentUserUsername } from '../../common/utils';
import usePins, { PIN_RESOURCES, MAX_PINS_ALLOWED } from '../../hooks/usePins';
Expand All @@ -41,6 +41,10 @@ import { getQuotaError } from '../common/quotaErrors'
const DEFAULT_LIMIT = 25;
const FILTERS_WIDTH = 250
const FILTERABLE_RESOURCES = ['concepts', 'mappings', 'repos', 'sources', 'collections', 'references']
const HIERARCHY_QUERY_PARAMS = ['q', 'page', 'limit', 'includeSearchMeta', 'onlyHierarchyRoot']

// Facet filters force the API onto the search-index path, which ignores onlyHierarchyRoot.
const toRequestParams = params => params.onlyHierarchyRoot ? pick(params, HIERARCHY_QUERY_PARAMS) : params

const getBaseCollectionUrl = url => {
const match = (url || '').match(/^(.*\/collections\/[^/]+\/)(?:[^/]+\/)?(?:concepts|mappings|references)\/?$/)
Expand Down Expand Up @@ -71,6 +75,19 @@ const Search = props => {
const [page, setPage] = React.useState(0);
const [pageSize, setPageSize] = React.useState(DEFAULT_LIMIT);
const [resource, setResource] = React.useState(false)
const isHierarchySupported = Boolean(props.nested && props.repo?.hierarchy_root_url && (resource || props.resource) === 'concepts')
const getDefaultDisplay = () => {
if(!props.nested || !props.repo || (resource || props.resource) !== 'concepts')
return undefined
const savedDisplay = props.repo.meta?.display?.default_view
const queryParams = new URLSearchParams(window.location.search)
if(savedDisplay === 'hierarchy' && isHierarchySupported && !queryParams.has('filters') && !queryParams.get('q'))
return 'hierarchy'
if(savedDisplay === 'card' && !props.noCardDisplay)
return 'card'
return 'table'
}
const [defaultDisplay] = React.useState(getDefaultDisplay)
const [result, setResult] = React.useState({})
const [filters, setFilters] = React.useState({})
const [selected, setSelected] = React.useState([])
Expand All @@ -85,11 +102,12 @@ const Search = props => {
const [transformingReferences, setTransformingReferences] = React.useState(false)
const [bulkRemoveOpen, setBulkRemoveOpen] = React.useState(false)
const [bulkRemoving, setBulkRemoving] = React.useState(false)
const [hierarchy, setHierarchy] = React.useState(false)
const [hierarchy, setHierarchy] = React.useState(defaultDisplay === 'hierarchy')

const didMount = React.useRef(false);
const pendingRepoDefaultFilters = React.useRef(false)
const isFilterable = _resource => FILTERABLE_RESOURCES.includes(_resource)
const isHierarchySupported = Boolean(props.nested && props.repo?.hierarchy_root_url && (resource || props.resource) === 'concepts')
const isHierarchyView = isHierarchySupported && hierarchy

React.useEffect(() => {
if(!props.url)
Expand Down Expand Up @@ -118,7 +136,13 @@ const Search = props => {
if(newHierarchy !== hierarchy) {
setHierarchy(newHierarchy)
setPage(1)
fetchResults(getQueryParams(input, 1, pageSize, filters, orderBy, order, newHierarchy), true)
const applyRepoDefaultFilters = !newHierarchy && pendingRepoDefaultFilters.current && !getFiltersFromQueryParams()
if(!newHierarchy)
pendingRepoDefaultFilters.current = false
if(applyRepoDefaultFilters)
history.replace(getCurrentLayoutURL(getQueryParams(input, 1, pageSize, getAppliedFacetFromQueryParam(props.repoDefaultFilters), orderBy, order, false)))
else
fetchResults(getQueryParams(input, 1, pageSize, filters, orderBy, order, newHierarchy), true)
}
}
input && setTimeout(highlight, 100)
Expand All @@ -134,7 +158,7 @@ const Search = props => {
_resource = 'orgs'
let url = '/search/?'
if(q)
url += `&q=${q || ''}`
url += `&q=${encodeURIComponent(q || '')}`
if(_resource !== 'concepts')
url += `&type=${_resource}`
if(limit !== DEFAULT_LIMIT)
Expand Down Expand Up @@ -216,7 +240,10 @@ const Search = props => {
_fetch = true
_fetchFacets = true
}
if(includeRepoDefaultFilters && !_filters && !isEmpty(props.repoDefaultFilters)) {
// The saved defaults are list filters: opening in Hierarchy leaves them for the first switch to Table or Card.
if(includeRepoDefaultFilters && !_filters && !isEmpty(props.repoDefaultFilters) && isHierarchyView)
pendingRepoDefaultFilters.current = true
else if(includeRepoDefaultFilters && !_filters && !isEmpty(props.repoDefaultFilters)) {
_filters = getAppliedFacetFromQueryParam(props.repoDefaultFilters)
// Write the defaults into the URL so it stays the single source of truth --
// otherwise the next location.search-driven sync sees no `filters` param and
Expand Down Expand Up @@ -326,7 +353,7 @@ const Search = props => {

const isSearchIndexQuery = () => {
const params = omit(
getQueryParams(input, page, pageSize, filters, orderBy, order),
toRequestParams(getQueryParams(input, page, pageSize, filters, orderBy, order)),
['page', 'limit', 'includeSearchMeta', 'onlyHierarchyRoot']
)
return Object.values(params).some(Boolean)
Expand Down Expand Up @@ -357,6 +384,7 @@ const Search = props => {
}

const fetchResults = (params, facets=true, _resource=undefined) => {
params = toRequestParams(params)
let __resource = _resource || resource
if(!__resource)
return
Expand Down Expand Up @@ -393,7 +421,7 @@ const Search = props => {
return {...result, [__resource]: resourceResult}
})
setLoading(false)
if(facets && isFilterable(__resource) && __resource !== 'references')
if(facets && !params.onlyHierarchyRoot && isFilterable(__resource) && __resource !== 'references')
fetchFacets(params, resourceResult, __resource)
})
}
Expand Down Expand Up @@ -512,7 +540,7 @@ const Search = props => {
}

const noResults = !loading && input && !(result[resource]?.results || []).length
const showFilters = openFilters && !noResults && isFilterable(resource)
const showFilters = openFilters && !noResults && isFilterable(resource) && !isHierarchyView

const getSearchResultsWidth = () => {
let toSubtract = 0;
Expand Down Expand Up @@ -756,6 +784,7 @@ const Search = props => {
<div className='col-xs-9 split' style={{width: getSearchResultsWidth(), paddingRight: 0, paddingLeft: 0, float: 'right', height: '100%'}}>
<div className='col-xs-12 padding-0' style={{height: '100%'}}>
<SearchResults
display={defaultDisplay}
loading={loading}
noCardDisplay={props.noCardDisplay}
order={order}
Expand All @@ -764,7 +793,7 @@ const Search = props => {
nested={props.nested}
openFilters={showFilters}
appliedFilters={filters}
isFilterable={isFilterable(resource)}
isFilterable={isFilterable(resource) && !isHierarchyView}
noResults={noResults}
searchedText={input}
bgColor={searchBgColor}
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,18 @@
"smart": "Smart Checksum"
},
"repo": {
"display": "Display",
"display_description": "How the Concepts tab opens for visitors",
"default_view": "Default view",
"default_view_table_description": "Rows with sortable columns",
"default_view_card_description": "One card per concept",
"default_view_hierarchy_description": "Tree from the hierarchy root",
"default_view_help": "Visitors can still switch views",
"default_view_needs_root": "Set a hierarchy root to open in Hierarchy. Until then, visitors see Table.",
"default_view_invalid_meta": "Fix the JSON in the Meta field to change this",
"default_filters": "Default filters (Table and Card)",
"default_filters_none": "None",
"default_filters_help": "Set these with \"Save as Default Filters\" in the filter panel on the Concepts tab. The Hierarchy view always shows the full tree.",
"repos": "Repositories",
"repo": "Repository",
"new_repo": "New Repository",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/locales/es/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,18 @@
"unresolved": "No resuelta"
},
"repo": {
"display": "Visualización",
"display_description": "Cómo se abre la pestaña Conceptos para los visitantes",
"default_view": "Vista predeterminada",
"default_view_table_description": "Filas con columnas que se pueden ordenar",
"default_view_card_description": "Una tarjeta por concepto",
"default_view_hierarchy_description": "Árbol desde la raíz de la jerarquía",
"default_view_help": "Los visitantes pueden cambiar de vista",
"default_view_needs_root": "Establezca una raíz de jerarquía para abrir en la vista Jerarquía. Hasta entonces, los visitantes verán la vista Tabla.",
"default_view_invalid_meta": "Corrija el JSON del campo Meta para cambiar esta opción",
"default_filters": "Filtros predeterminados (Tabla y Tarjeta)",
"default_filters_none": "Ninguno",
"default_filters_help": "Configúrelos con \"Guardar como Filtros Predeterminados\" en el panel de filtros de la pestaña Conceptos. La vista Jerarquía siempre muestra el árbol completo.",
"repos": "Repositorios",
"repo": "Repositorio",
"repo_type": "Tipo de Repositorio",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/locales/pt-BR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,18 @@
"smart": "Checksum inteligente"
},
"repo": {
"display": "Exibição",
"display_description": "Como a aba Conceitos é aberta para os visitantes",
"default_view": "Visualização padrão",
"default_view_table_description": "Linhas com colunas que podem ser ordenadas",
"default_view_card_description": "Um cartão por conceito",
"default_view_hierarchy_description": "Árvore a partir da raiz da hierarquia",
"default_view_help": "Os visitantes ainda podem alternar entre as visualizações",
"default_view_needs_root": "Defina uma raiz de hierarquia para abrir na visualização Hierarquia. Até lá, os visitantes verão a visualização Tabela.",
"default_view_invalid_meta": "Corrija o JSON no campo Meta para alterar esta opção",
"default_filters": "Filtros padrão (Tabela e Cartão)",
"default_filters_none": "Nenhum",
"default_filters_help": "Configure-os com \"Salvar como filtros padrão\" no painel de filtros da aba Conceitos. A visualização Hierarquia sempre mostra a árvore completa.",
"repos": "Repositórios",
"repo": "Repositório",
"new_repo": "Novo repositório",
Expand Down
12 changes: 12 additions & 0 deletions src/i18n/locales/zh/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,18 @@
"smart": "智能校验和"
},
"repo": {
"display": "显示",
"display_description": "访客打开“概念”选项卡时的显示方式",
"default_view": "默认视图",
"default_view_table_description": "按行显示,可按列排序",
"default_view_card_description": "每个概念显示为一张卡片",
"default_view_hierarchy_description": "从层级根节点展开的树形结构",
"default_view_help": "访客仍可切换视图",
"default_view_needs_root": "设置层级根节点后才能默认打开层级视图。在此之前,访客将看到表格视图。",
"default_view_invalid_meta": "请修正 Meta 字段中的 JSON 后再更改此选项",
"default_filters": "默认筛选条件(表格和卡片)",
"default_filters_none": "无",
"default_filters_help": "在“概念”选项卡的筛选面板中,使用“另存为默认筛选条件”进行设置。层级视图始终显示完整的树形结构。",
"repos": "存储库",
"repo": "存储库",
"repo_type": "存储库类型",
Expand Down
Loading