From 022597209bf6e17bb9c5e52c4f2b2646790acb20 Mon Sep 17 00:00:00 2001 From: Peter Giles <8978655+petertgiles@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:30:45 -0400 Subject: [PATCH 01/17] index page --- apps/web/src/components/Router.tsx | 4 ++ apps/web/src/hooks/useRoutes.ts | 2 + apps/web/src/pages/Skills/IndexSkillPage.tsx | 16 +++-- apps/web/src/pages/Skills/ViewSkillPage.tsx | 0 .../pages/Skills/components/SkillTable.tsx | 61 ++++++++++--------- 5 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 apps/web/src/pages/Skills/ViewSkillPage.tsx diff --git a/apps/web/src/components/Router.tsx b/apps/web/src/components/Router.tsx index 243c6908a71..890253b4091 100644 --- a/apps/web/src/components/Router.tsx +++ b/apps/web/src/components/Router.tsx @@ -839,6 +839,10 @@ const createRoute = (locale: Locales) => { path: ":skillId", children: [ + { + index: true, + lazy: () => import("../pages/Skills/ViewSkillPage"), + }, { path: "edit", lazy: () => import("../pages/Skills/UpdateSkillPage"), diff --git a/apps/web/src/hooks/useRoutes.ts b/apps/web/src/hooks/useRoutes.ts index df9bcb9777e..92669ed4604 100644 --- a/apps/web/src/hooks/useRoutes.ts +++ b/apps/web/src/hooks/useRoutes.ts @@ -132,6 +132,8 @@ const getRoutes = (lang: Locales) => { // Admin - Skills skillTable: () => [adminUrl, "settings", "skills"].join("/"), skillCreate: () => [adminUrl, "settings", "skills", "create"].join("/"), + skillView: (skillId: string) => + [adminUrl, "settings", "skills", skillId].join("/"), skillUpdate: (skillId: string) => [adminUrl, "settings", "skills", skillId, "edit"].join("/"), diff --git a/apps/web/src/pages/Skills/IndexSkillPage.tsx b/apps/web/src/pages/Skills/IndexSkillPage.tsx index bf0040455ac..b8fc4183c0d 100644 --- a/apps/web/src/pages/Skills/IndexSkillPage.tsx +++ b/apps/web/src/pages/Skills/IndexSkillPage.tsx @@ -3,12 +3,11 @@ import { useIntl } from "react-intl"; import { ROLE_NAME } from "@gc-digital-talent/auth"; import SEO from "~/components/SEO/SEO"; -import AdminContentWrapper from "~/components/AdminContentWrapper/AdminContentWrapper"; import useRoutes from "~/hooks/useRoutes"; import useBreadcrumbs from "~/hooks/useBreadcrumbs"; -import AdminHero from "~/components/HeroDeprecated/AdminHero"; import RequireAuth from "~/components/RequireAuth/RequireAuth"; import pageTitles from "~/messages/pageTitles"; +import Hero from "~/components/Hero"; import SkillTableApi from "./components/SkillTable"; @@ -31,13 +30,12 @@ export const IndexSkillPage = () => { return ( <> - - - - + +
+
+ +
+
); }; diff --git a/apps/web/src/pages/Skills/ViewSkillPage.tsx b/apps/web/src/pages/Skills/ViewSkillPage.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/web/src/pages/Skills/components/SkillTable.tsx b/apps/web/src/pages/Skills/components/SkillTable.tsx index 95b20add593..57e8766fcb3 100644 --- a/apps/web/src/pages/Skills/components/SkillTable.tsx +++ b/apps/web/src/pages/Skills/components/SkillTable.tsx @@ -15,12 +15,11 @@ import { useIntlLanguages, } from "@gc-digital-talent/i18n"; import { notEmpty, unpackMaybes } from "@gc-digital-talent/helpers"; -import { LoadingErrorMessage } from "@gc-digital-talent/ui"; +import { Link, LoadingErrorMessage } from "@gc-digital-talent/ui"; import { Skill, SkillCategory, graphql } from "@gc-digital-talent/graphql"; import useRoutes from "~/hooks/useRoutes"; import Table from "~/components/Table/ResponsiveTable/ResponsiveTable"; -import cells from "~/components/Table/cells"; import adminMessages from "~/messages/adminMessages"; import { normalizedText } from "~/components/Table/sortingFns"; import { @@ -31,7 +30,6 @@ import messages from "~/lang/frCompiled.json"; import { categoryAccessor, - descriptionCell, familiesAccessor, skillFamiliesCell, } from "./tableHelpers"; @@ -199,34 +197,27 @@ const SkillTable = ({ }, cell: ({ row: { original: skill } }) => { const skillName = getLocalizedName(skill.name, intl); - return isPublic - ? skillName - : cells.edit(skill.id, paths.skillTable(), skillName, skillName); + return isPublic ? ( + skillName + ) : ( + {skillName} + ); }, }), - columnHelper.accessor((skill) => familiesAccessor(skill, intl), { - id: "skillFamilies", - sortingFn: normalizedText, - header: intl.formatMessage(adminMessages.skillFamilies), - cell: ({ row: { original: skill } }) => - skillFamiliesCell(skill.families, intl), - }), - columnHelper.accessor(({ category }) => categoryAccessor(category, intl), { - id: "category", - sortingFn: normalizedText, - header: intl.formatMessage(adminMessages.category), - }), columnHelper.accessor( (skill) => getLocalizedName(skill.description, intl, true), { id: "description", sortingFn: normalizedText, - cell: ({ row: { original: skill } }) => - descriptionCell( - intl, - getLocalizedName(skill.name, intl), - getLocalizedName(skill.description, intl), - ), + cell: ({ row: { original: skill } }) => { + const characterCount = 32; + const description = getLocalizedName(skill.description, intl); + return description.length < characterCount ? ( + description + ) : ( + <>{description.slice(0, characterCount)}… + ); + }, header: intl.formatMessage({ defaultMessage: "Description", id: "9yGJ6k", @@ -235,6 +226,18 @@ const SkillTable = ({ }), }, ), + columnHelper.accessor((skill) => familiesAccessor(skill, intl), { + id: "skillFamilies", + sortingFn: normalizedText, + header: intl.formatMessage(adminMessages.skillFamilies), + cell: ({ row: { original: skill } }) => + skillFamiliesCell(skill.families, intl), + }), + columnHelper.accessor(({ category }) => categoryAccessor(category, intl), { + id: "category", + sortingFn: normalizedText, + header: intl.formatMessage(adminMessages.category), + }), ] as ColumnDef[]; useEffect(() => { @@ -287,9 +290,9 @@ const SkillTable = ({ search={{ internal: true, label: intl.formatMessage({ - defaultMessage: "Search skills", - id: "cWqtEU", - description: "Label for the skills table search input", + defaultMessage: "Search by keyword", + id: "PYMFoh", + description: "Label for the keyword search input", }), }} add={ @@ -298,8 +301,8 @@ const SkillTable = ({ linkProps: { href: paths.skillCreate(), label: intl.formatMessage({ - defaultMessage: "Create skill", - id: "71mPNh", + defaultMessage: "Create new skill", + id: "q5j7GV", description: "Title for Create skill", }), from: currentUrl, From c84e85a280a83b338359afa7fafcd1150371cc24 Mon Sep 17 00:00:00 2001 From: Peter Giles <8978655+petertgiles@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:42:49 -0400 Subject: [PATCH 02/17] view skill page --- apps/web/src/pages/Skills/ViewSkillPage.tsx | 272 ++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/apps/web/src/pages/Skills/ViewSkillPage.tsx b/apps/web/src/pages/Skills/ViewSkillPage.tsx index e69de29bb2d..cdb73c574cd 100644 --- a/apps/web/src/pages/Skills/ViewSkillPage.tsx +++ b/apps/web/src/pages/Skills/ViewSkillPage.tsx @@ -0,0 +1,272 @@ +import { useIntl } from "react-intl"; +import { useQuery } from "urql"; +import IdentificationIcon from "@heroicons/react/24/outline/IdentificationIcon"; + +import { commonMessages, getLocalizedName } from "@gc-digital-talent/i18n"; +import { + Pending, + NotFound, + Heading, + Link, + CardSectioned, + Chip, + Chips, +} from "@gc-digital-talent/ui"; +import { + FragmentType, + Scalars, + getFragment, + graphql, +} from "@gc-digital-talent/graphql"; +import { ROLE_NAME } from "@gc-digital-talent/auth"; +import { unpackMaybes } from "@gc-digital-talent/helpers"; + +import SEO from "~/components/SEO/SEO"; +import useRoutes from "~/hooks/useRoutes"; +import useRequiredParams from "~/hooks/useRequiredParams"; +import useBreadcrumbs from "~/hooks/useBreadcrumbs"; +import RequireAuth from "~/components/RequireAuth/RequireAuth"; +import pageTitles from "~/messages/pageTitles"; +import Hero from "~/components/Hero"; +import FieldDisplay from "~/components/ToggleForm/FieldDisplay"; +import adminMessages from "~/messages/adminMessages"; + +export const SkillView_Fragment = graphql(/* GraphQL */ ` + fragment SkillForm on Skill { + id + key + name { + en + fr + } + description { + en + fr + } + keywords { + en + fr + } + category { + label { + en + fr + } + } + families { + id + name { + en + fr + } + } + } +`); + +interface ViewSkillProps { + query: FragmentType; +} + +export const ViewSkillForm = ({ query }: ViewSkillProps) => { + const intl = useIntl(); + const paths = useRoutes(); + const skill = getFragment(SkillView_Fragment, query); + + const skillFamilies = skill.families ?? []; + return ( + <> +
+ + {intl.formatMessage({ + defaultMessage: "Skill information", + id: "08IbRz", + description: "Heading for the 'view a skill' form", + })} + +
+ + + + {skill.name.en} + + + {skill.name.fr} + + + {skill.description?.en} + + + {skill.description?.fr} + + + {skill.keywords?.en} + + + {skill.keywords?.fr} + +
+ + + {getLocalizedName(skill.category.label, intl)} + + +
+
+ + + {skillFamilies.map((family) => ( + + {getLocalizedName(family.name, intl)} + + ))} + + +
+
+ + {skill.key} + +
+
+ + + + {intl.formatMessage({ + defaultMessage: "Edit skill information", + id: "eJPU4G", + description: "Link to edit the currently viewed skill", + })} + + +
+ + ); +}; + +interface RouteParams extends Record { + skillId: Scalars["ID"]["output"]; +} + +const Skill_Query = graphql(/* GraphQL */ ` + query ViewSkillPage($id: UUID!) { + skill(id: $id) { + name { + en + fr + } + ...SkillForm + } + } +`); + +const ViewSkillPage = () => { + const intl = useIntl(); + const routes = useRoutes(); + const { skillId: skillId } = useRequiredParams("skillId"); + const [{ data: skillData, fetching, error }] = useQuery({ + query: Skill_Query, + variables: { id: skillId }, + }); + + const skillName = getLocalizedName(skillData?.skill?.name, intl); + + const navigationCrumbs = useBreadcrumbs({ + crumbs: [ + { + label: intl.formatMessage(pageTitles.skillsEditor), + url: routes.skillTable(), + }, + { + label: skillName, + url: routes.skillView(skillId), + }, + ], + isAdmin: true, + }); + + const navTabs = [ + { + url: routes.skillView(skillId), + label: intl.formatMessage({ + defaultMessage: "Skill information", + id: "/qXiOK", + description: "Nav tab label for skill information", + }), + }, + ]; + + return ( + <> + + +
+
+ + {skillData?.skill ? ( + + ) : ( + +

+ {intl.formatMessage( + { + defaultMessage: "Skill {skillId} not found.", + id: "953EAy", + description: "Message displayed for skill not found.", + }, + { skillId: skillId }, + )} +

+
+ )} +
+
+
+ + ); +}; + +export const Component = () => ( + + + +); + +Component.displayName = "AdminViewSkillPage"; + +export default ViewSkillPage; From 19d8187a9211af7aad0a1b36f4587f4e71225614 Mon Sep 17 00:00:00 2001 From: Peter Giles <8978655+petertgiles@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:49:35 -0400 Subject: [PATCH 03/17] update skill page --- apps/web/src/pages/Skills/UpdateSkillPage.tsx | 395 ++++++++++-------- apps/web/src/pages/Skills/ViewSkillPage.tsx | 1 - 2 files changed, 229 insertions(+), 167 deletions(-) diff --git a/apps/web/src/pages/Skills/UpdateSkillPage.tsx b/apps/web/src/pages/Skills/UpdateSkillPage.tsx index 04020179208..c18c87d71a6 100644 --- a/apps/web/src/pages/Skills/UpdateSkillPage.tsx +++ b/apps/web/src/pages/Skills/UpdateSkillPage.tsx @@ -5,6 +5,7 @@ import pick from "lodash/pick"; import sortBy from "lodash/sortBy"; import { useMutation, useQuery } from "urql"; import { useEffect } from "react"; +import IdentificationIcon from "@heroicons/react/24/outline/IdentificationIcon"; import { toast } from "@gc-digital-talent/toast"; import { @@ -22,8 +23,15 @@ import { errorMessages, commonMessages, getLocalizedName, + formMessages, } from "@gc-digital-talent/i18n"; -import { Pending, NotFound } from "@gc-digital-talent/ui"; +import { + Pending, + NotFound, + CardSectioned, + Heading, + Link, +} from "@gc-digital-talent/ui"; import { Skill, UpdateSkillInput, @@ -36,16 +44,16 @@ import { } from "@gc-digital-talent/graphql"; import { ROLE_NAME } from "@gc-digital-talent/auth"; +import FieldDisplay from "~/components/ToggleForm/FieldDisplay"; import SEO from "~/components/SEO/SEO"; import useRoutes from "~/hooks/useRoutes"; import useRequiredParams from "~/hooks/useRequiredParams"; -import AdminContentWrapper from "~/components/AdminContentWrapper/AdminContentWrapper"; import adminMessages from "~/messages/adminMessages"; import { parseKeywords } from "~/utils/skillUtils"; -import AdminHero from "~/components/HeroDeprecated/AdminHero"; import useBreadcrumbs from "~/hooks/useBreadcrumbs"; import RequireAuth from "~/components/RequireAuth/RequireAuth"; -import useReturnPath from "~/hooks/useReturnPath"; +import Hero from "~/components/Hero"; +import pageTitles from "~/messages/pageTitles"; import { SkillFormOptions_Query } from "./operations"; @@ -174,12 +182,10 @@ export const UpdateSkillForm = ({ const { handleSubmit } = methods; - const navigateTo = useReturnPath(paths.skillTable()); - const onSubmit: SubmitHandler = async (values: FormValues) => { return handleUpdateSkill(initialSkill.id, formValuesToSubmitData(values)) .then(() => { - navigate(navigateTo); + navigate(paths.skillView(initialSkill.id)); toast.success( intl.formatMessage({ defaultMessage: "Skill updated successfully!", @@ -209,132 +215,179 @@ export const UpdateSkillForm = ({ ); return ( -
- -
- - -