From 8e6cbbaf0725de544eb4838912d54b25e668662d Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Fri, 17 Mar 2023 15:24:32 +0100 Subject: [PATCH] feat: rsd admin can define software highlights --- data-generation/main.js | 18 +- .../004-create-relations-for-software.sql | 34 +++ database/020-row-level-security.sql | 13 + database/100-create-api-views.sql | 90 ++++++- frontend/components/admin/AdminNav.tsx | 14 +- .../components/admin/organisations/index.tsx | 2 +- .../software-domains/SoftwareDomainsPage.tsx | 12 - .../AddSoftwareHighlights.tsx | 116 +++++++++ .../SoftwareOptionFound.tsx | 57 +++++ .../SortableHighlightList.tsx | 111 ++++++++ .../SortableHightlightItem.tsx | 95 +++++++ .../apiSoftwareHighlights.tsx | 242 ++++++++++++++++++ .../admin/software-highlights/index.tsx | 44 ++++ .../feedback/FeedbackPanelButton.tsx | 6 +- frontend/pages/admin/software-domains.tsx | 55 ---- frontend/pages/admin/software-highlights.tsx | 32 +++ frontend/styles/README.md | 13 +- frontend/types/SoftwareTypes.ts | 5 +- frontend/utils/getSoftware.ts | 3 +- 19 files changed, 865 insertions(+), 97 deletions(-) delete mode 100644 frontend/components/admin/software-domains/SoftwareDomainsPage.tsx create mode 100644 frontend/components/admin/software-highlights/AddSoftwareHighlights.tsx create mode 100644 frontend/components/admin/software-highlights/SoftwareOptionFound.tsx create mode 100644 frontend/components/admin/software-highlights/SortableHighlightList.tsx create mode 100644 frontend/components/admin/software-highlights/SortableHightlightItem.tsx create mode 100644 frontend/components/admin/software-highlights/apiSoftwareHighlights.tsx create mode 100644 frontend/components/admin/software-highlights/index.tsx delete mode 100644 frontend/pages/admin/software-domains.tsx create mode 100644 frontend/pages/admin/software-highlights.tsx diff --git a/data-generation/main.js b/data-generation/main.js index afa094769..40a0d4e30 100644 --- a/data-generation/main.js +++ b/data-generation/main.js @@ -1,9 +1,9 @@ +// SPDX-FileCopyrightText: 2022 - 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2022 - 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 @@ -303,6 +303,15 @@ function generateSoftwareForSoftware(ids) { return result; } +function generateSoftwareHighlights(ids) { + const result = []; + for (let index = 0; index < ids.length; index++) { + const isHighlight = !!faker.helpers.maybe(() => true, {probability: 0.3}); + if (isHighlight === true) result.push({software: ids[index]}) + } + return result; +} + async function generateProjects(amount=500) { const result = []; @@ -665,6 +674,7 @@ const softwarePromise = postToBackend('/software', await generateSofware()) postToBackend('/keyword_for_software', generateKeywordsForEntity(idsSoftware, idsKeywords, 'software')); postToBackend('/mention_for_software', generateMentionsForEntity(idsSoftware, idsMentions, 'software')); postToBackend('/software_for_software', generateSoftwareForSoftware(idsSoftware)); + postToBackend('/software_highlight', generateSoftwareHighlights(idsSoftware.slice(0,10))); }); const projectPromise = postToBackend('/project', await generateProjects()) .then(resp => resp.json()) diff --git a/database/004-create-relations-for-software.sql b/database/004-create-relations-for-software.sql index f756a0d50..e1e43e9a1 100644 --- a/database/004-create-relations-for-software.sql +++ b/database/004-create-relations-for-software.sql @@ -190,3 +190,37 @@ END $$; CREATE TRIGGER sanitise_update_testimonial BEFORE UPDATE ON testimonial FOR EACH ROW EXECUTE PROCEDURE sanitise_update_testimonial(); + + + +CREATE TABLE software_highlight ( + software UUID REFERENCES software (id) PRIMARY KEY, + date_start DATE, + date_end DATE, + position INTEGER, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE FUNCTION sanitise_insert_software_highlight() RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.created_at = LOCALTIMESTAMP; + NEW.updated_at = NEW.created_at; + return NEW; +END +$$; + +CREATE TRIGGER sanitise_insert_software_highlight BEFORE INSERT ON software_highlight FOR EACH ROW EXECUTE PROCEDURE sanitise_insert_software_highlight(); + +CREATE FUNCTION sanitise_update_software_highlight() RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.software = OLD.software; + NEW.created_at = OLD.created_at; + NEW.updated_at = LOCALTIMESTAMP; + return NEW; +END +$$; + +CREATE TRIGGER sanitise_update_software_highlight BEFORE UPDATE ON software_highlight FOR EACH ROW EXECUTE PROCEDURE sanitise_update_software_highlight(); diff --git a/database/020-row-level-security.sql b/database/020-row-level-security.sql index 8d9c94746..dad0473b0 100644 --- a/database/020-row-level-security.sql +++ b/database/020-row-level-security.sql @@ -2,6 +2,8 @@ -- SPDX-FileCopyrightText: 2021 - 2023 Netherlands eScience Center -- SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -- SPDX-FileCopyrightText: 2022 - 2023 dv4all +-- SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) +-- SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -- -- SPDX-License-Identifier: Apache-2.0 @@ -673,3 +675,14 @@ CREATE POLICY anyone_can_read ON oaipmh FOR SELECT TO rsd_web_anon, rsd_user CREATE POLICY admin_all_rights ON oaipmh TO rsd_admin USING (TRUE) WITH CHECK (TRUE); + + +-- software_highlights +ALTER TABLE software_highlight ENABLE ROW LEVEL SECURITY; + +CREATE POLICY anyone_can_read ON software_highlight FOR SELECT TO rsd_web_anon, rsd_user + USING (software IN (SELECT id FROM software)); + +CREATE POLICY admin_all_rights ON software_highlight TO rsd_admin + USING (TRUE) + WITH CHECK (TRUE); diff --git a/database/100-create-api-views.sql b/database/100-create-api-views.sql index 36b6b7b19..f04a7d287 100644 --- a/database/100-create-api-views.sql +++ b/database/100-create-api-views.sql @@ -1,10 +1,9 @@ -- SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all) -- SPDX-FileCopyrightText: 2021 - 2023 dv4all --- SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) (dv4all) +-- SPDX-FileCopyrightText: 2022 - 2023 Christian Meeßen (GFZ) -- SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) +-- SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -- SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center --- SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) --- SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -- -- SPDX-License-Identifier: Apache-2.0 @@ -178,6 +177,52 @@ BEGIN END $$; +-- SOFTWARE OVERVIEW LIST FOR SEARCH +-- WITH COUNTS and KEYWORDS for filtering +CREATE FUNCTION software_search() RETURNS TABLE ( + id UUID, + slug VARCHAR, + brand_name VARCHAR, + short_statement VARCHAR, + image_id VARCHAR, + updated_at TIMESTAMPTZ, + contributor_cnt BIGINT, + mention_cnt BIGINT, + is_published BOOLEAN, + keywords CITEXT[], + keywords_text TEXT, + prog_lang TEXT[] +) LANGUAGE plpgsql STABLE AS +$$ +BEGIN + RETURN QUERY + SELECT + software.id, + software.slug, + software.brand_name, + software.short_statement, + software.image_id, + software.updated_at, + count_software_countributors.contributor_cnt, + count_software_mentions.mention_cnt, + software.is_published, + keyword_filter_for_software.keywords, + keyword_filter_for_software.keywords_text, + prog_lang_filter_for_software.prog_lang + FROM + software + LEFT JOIN + count_software_countributors() ON software.id=count_software_countributors.software + LEFT JOIN + count_software_mentions() ON software.id=count_software_mentions.software + LEFT JOIN + keyword_filter_for_software() ON software.id=keyword_filter_for_software.software + LEFT JOIN + prog_lang_filter_for_software() ON software.id=prog_lang_filter_for_software.software + ; +END +$$; + -- RELATED SOFTWARE LIST WITH COUNTS CREATE FUNCTION related_software_for_software(software_id UUID) RETURNS TABLE ( @@ -1380,7 +1425,6 @@ CREATE VIEW user_count_per_home_organisation AS home_organisation ; - -- Return the number of accounts since specified time stamp CREATE FUNCTION new_accounts_count_since_timestamp(timestmp TIMESTAMPTZ) RETURNS INTEGER LANGUAGE sql SECURITY DEFINER STABLE AS @@ -1393,7 +1437,6 @@ WHERE created_at > timestmp; $$; - -- Keywords use by software and projects -- DEPENDS ON FUNCTIONS keyword_count_for_software and keyword_count_for_projects CREATE FUNCTION keyword_cnt() RETURNS TABLE ( @@ -1447,4 +1490,41 @@ ORDER BY COUNT(*) DESC LIMIT 1; +; +$$; + +-- Get a list of all software highlights with latest highlights first +CREATE FUNCTION software_for_highlight() RETURNS TABLE ( + id UUID, + slug VARCHAR, + brand_name VARCHAR, + short_statement VARCHAR, + image_id VARCHAR, + is_published BOOLEAN, + contributor_cnt BIGINT, + mention_cnt BIGINT, + "position" INTEGER, + updated_at TIMESTAMPTZ +) LANGUAGE sql STABLE AS +$$ +SELECT + software.id, + software.slug, + software.brand_name, + software.short_statement, + software.image_id, + software.is_published, + count_software_countributors.contributor_cnt, + count_software_mentions.mention_cnt, + software_highlight.position, + software_highlight.updated_at +FROM + software +INNER JOIN + software_highlight ON software.id=software_highlight.software +LEFT JOIN + count_software_countributors() ON software.id=count_software_countributors.software +LEFT JOIN + count_software_mentions() ON software.id=count_software_mentions.software +; $$; diff --git a/frontend/components/admin/AdminNav.tsx b/frontend/components/admin/AdminNav.tsx index ff9550143..bbc74a683 100644 --- a/frontend/components/admin/AdminNav.tsx +++ b/frontend/components/admin/AdminNav.tsx @@ -13,10 +13,10 @@ import ListItemText from '@mui/material/ListItemText' import DescriptionIcon from '@mui/icons-material/Description' import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck' import PersonRemoveIcon from '@mui/icons-material/PersonRemove' -import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd' import SpellcheckIcon from '@mui/icons-material/Spellcheck' import DomainAddIcon from '@mui/icons-material/DomainAdd' import AccountCircleIcon from '@mui/icons-material/AccountCircle' +import FluorescentIcon from '@mui/icons-material/Fluorescent' export const adminPages = { pages:{ @@ -25,6 +25,12 @@ export const adminPages = { icon: , path: '/admin/public-pages', }, + softwareHighlights:{ + title: 'Software highlights', + subtitle: 'Manage software highlights', + icon: , + path: '/admin/software-highlights', + }, orcid:{ title: 'ORCID users', subtitle: 'Manage ORCID users', @@ -55,12 +61,6 @@ export const adminPages = { icon: , path: '/admin/keywords', }, - softwareDomains:{ - title: 'Software domains', - subtitle: 'Manage software domains', - icon: , - path: '/admin/software-domains', - }, } // extract page types from the object diff --git a/frontend/components/admin/organisations/index.tsx b/frontend/components/admin/organisations/index.tsx index ed20f9217..126625922 100644 --- a/frontend/components/admin/organisations/index.tsx +++ b/frontend/components/admin/organisations/index.tsx @@ -5,12 +5,12 @@ import {useContext} from 'react' +import {useSession} from '~/auth' import Pagination from '~/components/pagination/Pagination' import Searchbox from '~/components/search/Searchbox' import PaginationContext from '~/components/pagination/PaginationContext' import OrganisationsAdminList from './OrganisationsAdminList' import AddOrganisation from './AddOrganisation' -import {useSession} from '~/auth' import {useOrganisations} from './apiOrganisation' export default function OrganisationAdminPage() { diff --git a/frontend/components/admin/software-domains/SoftwareDomainsPage.tsx b/frontend/components/admin/software-domains/SoftwareDomainsPage.tsx deleted file mode 100644 index cd21caf3c..000000000 --- a/frontend/components/admin/software-domains/SoftwareDomainsPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2023 dv4all -// -// SPDX-License-Identifier: Apache-2.0 - -export default function SoftwareDomainsPage() { - return ( -
-

Custom categories - under construction

-
- ) -} diff --git a/frontend/components/admin/software-highlights/AddSoftwareHighlights.tsx b/frontend/components/admin/software-highlights/AddSoftwareHighlights.tsx new file mode 100644 index 000000000..2dcf39d92 --- /dev/null +++ b/frontend/components/admin/software-highlights/AddSoftwareHighlights.tsx @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import {HTMLAttributes, useState} from 'react' + +import {SoftwareListItem} from '~/types/SoftwareTypes' +import {getBaseUrl} from '~/utils/fetchHelpers' +import {getSoftwareList} from '~/utils/getSoftware' +import {softwareListUrl} from '~/utils/postgrestUrl' +import AsyncAutocompleteSC, {AutocompleteOption} from '~/components/form/AsyncAutocompleteSC' +import EditSectionTitle from '~/components/layout/EditSectionTitle' +import SoftwareOptionFound from './SoftwareOptionFound' +import {SoftwareHighlight} from './apiSoftwareHighlights' +import {itemsNotInReferenceList} from '~/utils/itemsNotInReferenceList' + +type AddSoftwareHighlightsProps = { + highlights: SoftwareHighlight[] + onAddSoftware: (id:string)=>void +} + +export default function AddSoftwareHighlights({onAddSoftware,highlights}:AddSoftwareHighlightsProps) { + const [options, setOptions] = useState[]>([]) + const [status, setStatus] = useState<{ + loading: boolean, + foundFor: string | undefined + }>({ + loading: false, + foundFor: undefined + }) + + async function searchSoftware(searchFor: string) { + setStatus({loading: true, foundFor: undefined}) + + const url = softwareListUrl({ + baseUrl: getBaseUrl(), + search: searchFor, + order: 'mention_cnt.desc.nullslast,contributor_cnt.desc.nullslast,updated_at.desc.nullslast', + limit: 50, + offset: 0, + }) + // get software list, we do not pass the token + // when token is passed it will return not published items too + const resp= await getSoftwareList({url}) + // remove items already in hightlights + const software = itemsNotInReferenceList({ + list: resp.data ?? [], + referenceList: highlights ?? [], + key: 'id' + }) + + const options = software.map(item => ({ + key: item.slug, + label: item.brand_name, + data: item + })) + // set options + setOptions(options ?? []) + // stop loading + setStatus({ + loading: false, + foundFor: searchFor + }) + } + + function renderOption(props: HTMLAttributes, + option: AutocompleteOption, + state: object) { + // when value is not not found option returns input prop + if (option?.input) { + // we DO NOT offer an option to create this entry + return null + } + return ( +
  • + +
  • + ) + } + + function onAdd(selected:AutocompleteOption) { + if (selected && selected.data) { + onAddSoftware(selected.data.id) + } + } + + return ( +
    + + + +
    + ) +} diff --git a/frontend/components/admin/software-highlights/SoftwareOptionFound.tsx b/frontend/components/admin/software-highlights/SoftwareOptionFound.tsx new file mode 100644 index 000000000..87f3140a4 --- /dev/null +++ b/frontend/components/admin/software-highlights/SoftwareOptionFound.tsx @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 - 2023 dv4all +// SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import ListItem from '@mui/material/ListItem' +import ListItemAvatar from '@mui/material/ListItemAvatar' +import Avatar from '@mui/material/Avatar' +import ListItemText from '@mui/material/ListItemText' + +import {SoftwareListItem} from '~/types/SoftwareTypes' +import {AutocompleteOption} from '~/types/AutocompleteOptions' +import {getImageUrl} from '~/utils/editImage' + +export default function SoftwareOptionFound({option}: { option: AutocompleteOption }) { + + const {brand_name, contributor_cnt, mention_cnt, image_id, is_published} = option.data + + return ( + + + + {brand_name.slice(0,3)} + + + + Mentions: {mention_cnt ?? 0} + Contributors: {contributor_cnt ?? 0} + Published: {is_published ? 'Yes' : 'No'} + + } + /> + + ) +} diff --git a/frontend/components/admin/software-highlights/SortableHighlightList.tsx b/frontend/components/admin/software-highlights/SortableHighlightList.tsx new file mode 100644 index 000000000..14a77d27d --- /dev/null +++ b/frontend/components/admin/software-highlights/SortableHighlightList.tsx @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import {useState} from 'react' +import Alert from '@mui/material/Alert' +import AlertTitle from '@mui/material/AlertTitle' + +import ContentLoader from '~/components/layout/ContentLoader' +import ConfirmDeleteModal from '~/components/layout/ConfirmDeleteModal' +import {SoftwareHighlight} from './apiSoftwareHighlights' +import SortableList from '~/components/layout/SortableList' +import SortableHighlightItem from './SortableHightlightItem' +import {useRouter} from 'next/router' + +type DeleteOrganisationModal = { + open: boolean, + highlight?: SoftwareHighlight +} + +type SoftwareHighlightsListProps = { + highlights: SoftwareHighlight[] + loading: boolean + onDelete: (id: string) => void + onSorted: (highlights:SoftwareHighlight[])=>void +} + +export default function SortableHighlightsList({highlights, loading, onSorted, onDelete}: SoftwareHighlightsListProps) { + const router = useRouter() + const [modal, setModal] = useState({ + open: false + }) + + if (loading) return + + if (highlights.length === 0) { + return ( + + No software highlights + You can add software to highlight section using search on the right. + + ) + } + + function onEdit(pos: number) { + const highlight = highlights[pos] + if (highlight) { + router.push(`/software/${highlight.slug}/edit`) + } + } + + function confirmDelete(pos: number) { + const highlight = highlights[pos] + if (highlight) { + setModal({ + open: true, + highlight + }) + } + } + + function onRenderItem(item: SoftwareHighlight, index?: number) { + return ( + + ) + } + + return ( + <> + + + +

    + Are you sure you want to delete software {modal?.highlight?.brand_name} from hightlight? +

    + + } + onCancel={() => { + setModal({ + open: false + }) + }} + onDelete={() => { + if (modal?.highlight?.id) { + onDelete(modal?.highlight?.id) + } + setModal({ + open: false + }) + }} + /> + + ) +} diff --git a/frontend/components/admin/software-highlights/SortableHightlightItem.tsx b/frontend/components/admin/software-highlights/SortableHightlightItem.tsx new file mode 100644 index 000000000..043660d05 --- /dev/null +++ b/frontend/components/admin/software-highlights/SortableHightlightItem.tsx @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import ListItem from '@mui/material/ListItem' +import ListItemText from '@mui/material/ListItemText' +import ListItemAvatar from '@mui/material/ListItemAvatar' + +import {useSortable} from '@dnd-kit/sortable' +import {CSS} from '@dnd-kit/utilities' +import SortableListItemActions from '~/components/layout/SortableListItemActions' +import useMediaQuery from '@mui/material/useMediaQuery' +import {getImageUrl} from '~/utils/editImage' +import {SoftwareHighlight} from './apiSoftwareHighlights' +import Avatar from '@mui/material/Avatar' + +type HighlightProps = { + pos: number, + item: SoftwareHighlight, + onEdit: (pos: number) => void, + onDelete: (pos: number) => void, +} + + +export default function SortableHighlightItem({pos, item, onEdit, onDelete}: HighlightProps) { + const smallScreen = useMediaQuery('(max-width:600px)') + const { + attributes,listeners,setNodeRef, + transform,transition,isDragging + } = useSortable({id: item.id ?? ''}) + const {brand_name, contributor_cnt, mention_cnt, slug, image_id, is_published} = item + + return ( + + } + sx={{ + // this makes space for buttons + paddingRight: '11rem', + // height:'5rem', + '&:hover': { + backgroundColor:'grey.100' + }, + transform: CSS.Translate.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + backgroundColor: isDragging ? 'grey.100' : 'paper', + zIndex: isDragging ? 9:0, + cursor: isDragging ? 'move' : 'default' + }} + > + {smallScreen ? null : + + + {brand_name.slice(0,3)} + + + } + + Mentions: {mention_cnt ?? 0} + Contributors: {contributor_cnt ?? 0} + Published: {is_published ? 'Yes' : 'No'} + + } + /> + + ) +} diff --git a/frontend/components/admin/software-highlights/apiSoftwareHighlights.tsx b/frontend/components/admin/software-highlights/apiSoftwareHighlights.tsx new file mode 100644 index 000000000..58f8e8de4 --- /dev/null +++ b/frontend/components/admin/software-highlights/apiSoftwareHighlights.tsx @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import {useCallback, useEffect, useState} from 'react' +import useSnackbar from '~/components/snackbar/useSnackbar' +import {extractCountFromHeader} from '~/utils/extractCountFromHeader' +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import logger from '~/utils/logger' +import {paginationUrlParams} from '~/utils/postgrestUrl' +import usePaginationWithSearch from '~/utils/usePaginationWithSearch' + +export type SoftwareHighlight = { + id:string, + slug: string, + brand_name: string, + short_statement: string, + image_id: string, + updated_at: string, + contributor_cnt: number, + mention_cnt: number, + is_published: boolean, + position: number | null +} + +type getHighlightsApiParams = { + token: string, + page: number + rows: number + searchFor?: string + orderBy?: string +} + +export function useSoftwareHighlights(token: string) { + const {showErrorMessage} = useSnackbar() + const {searchFor, page, rows, count, setCount} = usePaginationWithSearch('Find highlight by name') + const [highlights, setHighlights] = useState([]) + const [loading, setLoading] = useState(true) + + const loadHighlight = useCallback(async() => { + setLoading(true) + const {highlights, count} = await getHighlights({ + token, + searchFor, + page, + rows + }) + setHighlights(highlights) + setCount(count ?? 0) + setLoading(false) + // we do not include setCount in order to avoid loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token, searchFor, page, rows]) + + + useEffect(() => { + if (token) { + loadHighlight() + } + }, [loadHighlight, token]) + + async function addHighlight(id: string) { + const resp = await addSoftwareHighlight({ + id, + token + }) + + if (resp.status !== 200) { + showErrorMessage(`Failed to add software to highlight. ${resp.message}`) + } else { + await loadHighlight() + } + } + + async function deleteHighlight(id: string) { + const resp = await deleteSoftwareHighlight({ + id, + token + }) + + if (resp.status !== 200) { + showErrorMessage(`Failed to remove highlight. ${resp.message}`) + } else { + // remove item and renumber positions + const newList = highlights + .filter(item => item.id !== id) + .map((item, pos) => { + // renumber + item.position = pos + 1 + return item + }) + // update position in db + if (newList.length > 0) { + await sortHighlights(newList) + } else { + // we do not have highlights left + setHighlights([]) + } + } + } + + async function sortHighlights(items: SoftwareHighlight[]) { + // visually confirm position change + setHighlights(items) + // make all request + const resp = await patchSoftwareHighlights({ + highlights: items, + token + }) + if (resp.status !== 200) { + showErrorMessage(`Failed to sort highlight. ${resp.message}`) + // revert back in case of error + setHighlights(highlights) + } + } + + return { + count, + loading, + highlights, + addHighlight, + sortHighlights, + deleteHighlight + } +} + +async function getHighlights({page, rows, token, searchFor,orderBy}:getHighlightsApiParams) { + try { + let query = paginationUrlParams({rows, page}) + if (searchFor) { + query+=`&or=(brand_name.ilike.*${searchFor}*,short_statement.ilike.*${searchFor}*)` + } + if (orderBy) { + query+=`&order=${orderBy}` + } else { + query+='&order=position.asc,brand_name.asc' + } + // complete url + const url = `${getBaseUrl()}/rpc/software_for_highlight?${query}` + + // make request + const resp = await fetch(url,{ + method: 'GET', + headers: { + ...createJsonHeaders(token), + // request record count to be returned + // note: it's returned in the header + 'Prefer': 'count=exact' + }, + }) + + if ([200,206].includes(resp.status)) { + const highlights: SoftwareHighlight[] = await resp.json() + return { + count: extractCountFromHeader(resp.headers) ?? 0, + highlights + } + } + logger(`getHighlights: ${resp.status}: ${resp.statusText}`,'warn') + return { + count: 0, + highlights: [] + } + + } catch (e: any) { + return { + count: 0, + highlights: [] + } + } +} + +async function addSoftwareHighlight({id,token}:{id:string,token:string}) { + try { + const resp = await fetch('/api/v1/software_highlight', { + body: JSON.stringify({software:id}), + headers: createJsonHeaders(token), + method: 'POST' + }) + + return extractReturnMessage(resp) + + } catch (e: any) { + return { + status: 500, + message: e.message + } + } +} + +async function patchSoftwareHighlights({highlights,token}:{highlights:SoftwareHighlight[],token:string}) { + try { + // create all requests + const requests = highlights.map(item => { + const url = `/api/v1/software_highlight?software=eq.${item.id}` + return fetch(url, { + method: 'PATCH', + headers: { + ...createJsonHeaders(token), + }, + // just update position! + body: JSON.stringify({ + position: item.position + }) + }) + }) + // execute them in parallel + const responses = await Promise.all(requests) + // check for errors + return extractReturnMessage(responses[0]) + } catch (e: any) { + logger(`patchSoftwareHighlights: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} + +async function deleteSoftwareHighlight({id,token}:{id: string, token:string}) { + try { + const query = `software=eq.${id}` + const url = `${getBaseUrl()}/software_highlight?${query}` + + const resp = await fetch(url, { + method: 'DELETE', + headers: { + ...createJsonHeaders(token) + } + }) + + return extractReturnMessage(resp) + + } catch (e: any) { + return { + status: 500, + message: e.message + } + } +} + diff --git a/frontend/components/admin/software-highlights/index.tsx b/frontend/components/admin/software-highlights/index.tsx new file mode 100644 index 000000000..45b35b252 --- /dev/null +++ b/frontend/components/admin/software-highlights/index.tsx @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import {useContext} from 'react' + +import {useSession} from '~/auth' +import PaginationContext from '~/components/pagination/PaginationContext' +import {useSoftwareHighlights} from './apiSoftwareHighlights' +import AddSoftwareHighlights from './AddSoftwareHighlights' +import SortableHighlightsList from './SortableHighlightList' + +export default function AdminSoftwareHighlight() { + const {token} = useSession() + const {pagination:{count}} = useContext(PaginationContext) + const {highlights, loading, addHighlight, sortHighlights, deleteHighlight} = useSoftwareHighlights(token) + + // console.group('OrganisationAdminPage') + // console.log('organisations...', organisations) + // console.groupEnd() + + return ( +
    +
    +

    + Highlights + {highlights.length} +

    + + +
    + +
    + ) +} diff --git a/frontend/components/feedback/FeedbackPanelButton.tsx b/frontend/components/feedback/FeedbackPanelButton.tsx index 0226606b0..f19565745 100644 --- a/frontend/components/feedback/FeedbackPanelButton.tsx +++ b/frontend/components/feedback/FeedbackPanelButton.tsx @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 - 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 @@ -72,7 +72,7 @@ User Agent: ${navigator.userAgent}`
    diff --git a/frontend/pages/admin/software-domains.tsx b/frontend/pages/admin/software-domains.tsx deleted file mode 100644 index 8384be7bf..000000000 --- a/frontend/pages/admin/software-domains.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2023 dv4all -// -// SPDX-License-Identifier: Apache-2.0 - -import Head from 'next/head' -import {GetServerSidePropsContext} from 'next/types' - -import {app} from '../../config/app' -import DefaultLayout from '~/components/layout/DefaultLayout' -import AdminPageWithNav from '~/components/admin/AdminPageWithNav' -import {adminPages} from '~/components/admin/AdminNav' -import SoftwareDomainsPage from '~/components/admin/software-domains/SoftwareDomainsPage' - -const pageTitle = `${adminPages['softwareDomains'].title} | Admin page | ${app.title}` - -export default function AdminSoftwareDomains(props:any) { - - // console.group('AdminSoftwareDomains') - // console.log('domains...', domains) - // console.groupEnd() - - return ( - - - {pageTitle} - - - - - - ) -} - -// see documentation https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering -export async function getServerSideProps(context:GetServerSidePropsContext) { - try{ - const {req} = context - const token = req?.cookies['rsd_token'] - - // get links to all pages server side - // const links = await getPageLinks({is_published: false, token}) - - return { - // passed to the page component as props - props: { - domains:[] - }, - } - }catch(e){ - return { - notFound: true, - } - } -} diff --git a/frontend/pages/admin/software-highlights.tsx b/frontend/pages/admin/software-highlights.tsx new file mode 100644 index 000000000..ca13b1b94 --- /dev/null +++ b/frontend/pages/admin/software-highlights.tsx @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import Head from 'next/head' + +import {app} from '../../config/app' +import DefaultLayout from '~/components/layout/DefaultLayout' +import AdminPageWithNav from '~/components/admin/AdminPageWithNav' +import {adminPages} from '~/components/admin/AdminNav' +import AdminSoftwareHighlight from '~/components/admin/software-highlights/index' + +const pageTitle = `${adminPages['organisations'].title} | Admin page | ${app.title}` + +export default function AdminSoftwareHighlightsPage() { + + // console.group('AdminOrganisationsPage') + // console.log('domains...', domains) + // console.groupEnd() + + return ( + + + {pageTitle} + + + + + + ) +} diff --git a/frontend/styles/README.md b/frontend/styles/README.md index d8a059df0..80c2a4c14 100644 --- a/frontend/styles/README.md +++ b/frontend/styles/README.md @@ -1,6 +1,6 @@ @@ -170,10 +170,11 @@ const rsdTheme = { keys: ["xs", "sm", "md", "lg", "xl"], values: { xs: 0, - sm: 600, - md: 900, - lg: 1200, - xl: 1536, + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + '2xl':1536 }, unit: "px", }, diff --git a/frontend/types/SoftwareTypes.ts b/frontend/types/SoftwareTypes.ts index 79fea0d7a..ad8432152 100644 --- a/frontend/types/SoftwareTypes.ts +++ b/frontend/types/SoftwareTypes.ts @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: 2022 - 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2022 - 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 @@ -73,6 +73,7 @@ export type SoftwareListItem = { mention_cnt: number | null is_published: boolean is_featured?: boolean + image_id?: string | null } diff --git a/frontend/utils/getSoftware.ts b/frontend/utils/getSoftware.ts index e8449401f..3bd80ea44 100644 --- a/frontend/utils/getSoftware.ts +++ b/frontend/utils/getSoftware.ts @@ -5,12 +5,11 @@ // // SPDX-License-Identifier: Apache-2.0 +import logger from './logger' import {KeywordForSoftware, RepositoryInfo, SoftwareItem, SoftwareListItem} from '../types/SoftwareTypes' import {extractCountFromHeader} from './extractCountFromHeader' -import logger from './logger' import {createJsonHeaders, getBaseUrl} from './fetchHelpers' import {RelatedProjectForSoftware} from '~/types/Project' -import {SoftwareReleaseInfo} from '~/components/organisation/releases/useSoftwareReleases' /* * Software list for the software overview page