diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index b52baf57b6..0000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - ignorePatterns: ['**/h5pResizer.ts'], - extends: ['eslint-config-ndla'], - rules: { - 'react/prop-types': [2, { ignore: ['children', 'className', 't'] }], - }, -}; diff --git a/e2e/specs/grid.spec.ts b/e2e/specs/grid.spec.ts index 3106811e50..10185634f1 100644 --- a/e2e/specs/grid.spec.ts +++ b/e2e/specs/grid.spec.ts @@ -21,29 +21,30 @@ test("can select multiple different sizes", async ({ page }) => { expect(await page.getByTestId("slate-grid-cell").count()).toEqual(2); await page.getByTestId("edit-grid-button").click(); await page.getByRole("radiogroup").getByText("4").click(); - await page.getByRole("button", { name: "Lagre", exact: true }).click(); + await page.getByTestId("grid-form-save-button").click(); expect(await page.getByTestId("slate-grid-cell").count()).toEqual(4); await page.getByTestId("edit-grid-button").click(); await page.getByRole("radiogroup").getByText("2x2").click(); - await page.getByRole("button", { name: "Lagre", exact: true }).click(); + await page.getByTestId("grid-form-save-button").click(); expect(await page.getByTestId("slate-grid-cell").count()).toEqual(4); await page.getByTestId("edit-grid-button").click(); await page.getByRole("radiogroup").getByText("2", { exact: true }).click(); - await page.getByRole("button", { name: "Lagre", exact: true }).click(); + await page.getByTestId("grid-form-save-button").click(); expect(await page.getByTestId("slate-grid-cell").count()).toEqual(2); }); test("can change background color", async ({ page }) => { await page.getByTestId("edit-grid-button").click(); await page.getByText("Hvit").click(); - await page.getByRole("button", { name: "Lagre", exact: true }).click(); + await page.getByTestId("grid-form-save-button").click(); }); test("can set border", async ({ page }) => { await page.getByTestId("edit-grid-button").click(); let checkbox = page.locator('[data-scope="checkbox"][data-part="root"]'); await checkbox.click(); - await page.getByRole("button", { name: "Lagre", exact: true }).click(); + await page.getByTestId("grid-form-save-button").click(); + await page.getByTestId("edit-grid-button").click(); checkbox = page.locator('[data-scope="checkbox"][data-part="root"][data-state="checked"]'); await expect(checkbox).toBeVisible(); diff --git a/e2e/specs/search_audio.spec.ts b/e2e/specs/search_audio.spec.ts index c6331defde..9c5a62b1c5 100644 --- a/e2e/specs/search_audio.spec.ts +++ b/e2e/specs/search_audio.spec.ts @@ -27,19 +27,21 @@ test("Can use text input", async ({ page }) => { }); test("Can use audiotype dropdown", async ({ page }) => { - await page.locator('select[name="audio-type"]').selectOption({ label: "Podkast" }); + await page.getByTestId("audio-type-select").click(); + await page.getByRole("option", { name: "Podkast", exact: true }).click(); await page.getByTestId("audio-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("114"); - await page.locator('select[name="audio-type"]').selectOption({ label: "Velg lydfiltype" }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("audio-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(totalSearchCount); }); test("Can use language dropdown", async ({ page }) => { - await page.locator('select[name="language"]').selectOption({ label: "Engelsk" }); + await page.getByTestId("language-select").click(); + await page.getByRole("option", { name: "Engelsk", exact: true }).click(); await page.getByTestId("audio-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("315"); - await page.locator('select[name="language"]').selectOption({ label: "Velg språk" }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("audio-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(totalSearchCount); }); diff --git a/e2e/specs/search_content.spec.ts b/e2e/specs/search_content.spec.ts index 31c84cd2e5..461ad89cfe 100644 --- a/e2e/specs/search_content.spec.ts +++ b/e2e/specs/search_content.spec.ts @@ -27,55 +27,61 @@ test("Can use text input", async ({ page }) => { }); test("Can use status dropdown", async ({ page }) => { - await page.locator('select[name="draft-status"]').selectOption({ label: "Publisert" }); + await page.getByTestId("draft-status-select").click(); + await page.getByRole("option", { name: "Publisert", exact: true }).click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("17854"); - await page.locator('select[name="draft-status"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(searchTotalCount); }); test("Can use language dropdown", async ({ page }) => { - await page.locator('select[name="language"]').selectOption({ index: 1 }); + await page.getByTestId("language-select").click(); + await page.getByRole("option", { name: "Bokmål", exact: true }).click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("24326"); - await page.locator('select[name="language"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(searchTotalCount); }); test("Can use subject dropdown", async ({ page }) => { - await page.locator('select[name="subjects"]').selectOption({ label: "Biologi 1" }); + await page.getByTestId("subjects-select").click(); + await page.getByRole("option", { name: "Biologi 1", exact: true }).click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("620"); - await page.locator('select[name="subjects"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(searchTotalCount); }); test("Can use responsible dropdown", async ({ page }) => { - await page.locator('select[name="responsible-ids"]').selectOption({ label: "Ed Test" }); + await page.getByTestId("responsible-ids-select").click(); + await page.getByRole("option", { name: "Ed Test", exact: true }).click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("33"); - await page.locator('select[name="responsible-ids"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(searchTotalCount); }); test("Can use user dropdown", async ({ page }) => { - await page.locator('select[name="users"]').selectOption({ label: "Ed Test" }); + await page.getByTestId("users-select").click(); + await page.getByRole("option", { name: "Ed Test", exact: true }).click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("108"); - await page.locator('select[name="users"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(searchTotalCount); }); test("Can use content type dropdown", async ({ page }) => { - await page.locator('select[name="resource-types"]').selectOption({ label: "Arbeidsoppdrag" }); + await page.getByTestId("resource-types-select").click(); + await page.getByRole("option", { name: "Arbeidsoppdrag", exact: true }).click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("551"); - await page.locator('select[name="resource-types"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("content-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(searchTotalCount); }); diff --git a/e2e/specs/search_image.spec.ts b/e2e/specs/search_image.spec.ts index c669393b98..2c4e332e7f 100644 --- a/e2e/specs/search_image.spec.ts +++ b/e2e/specs/search_image.spec.ts @@ -27,28 +27,31 @@ test("Can use text input", async ({ page }) => { }); test("Can use model released dropdown", async ({ page }) => { - await page.locator('select[name="model-released"]').selectOption({ index: 1 }); + await page.getByTestId("model-released-select").click(); + await page.getByRole("option", { name: "Modellklarert", exact: true }).click(); await page.getByTestId("image-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("2248"); - await page.locator('select[name="model-released"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("image-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(totalSearchCount); }); test("Can use language dropdown", async ({ page }) => { - await page.locator('select[name="language"]').selectOption({ index: 1 }); + await page.getByTestId("language-select").click(); + await page.getByRole("option", { name: "Bokmål", exact: true }).click(); await page.getByTestId("image-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("26966"); - await page.locator('select[name="language"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("image-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(totalSearchCount); }); test("Can use license dropdown", async ({ page }) => { - await page.locator('select[name="license"]').selectOption({ index: 1 }); + await page.getByTestId("license-select").click(); + await page.getByRole("option", { name: "CC0 Public domain dedication: Gitt til fellesskapet", exact: true }).click(); await page.getByTestId("image-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual("906"); - await page.locator('select[name="license"]').selectOption({ index: 0 }); + await page.getByTestId("remove-tag-button").click(); await page.getByTestId("image-search-result").first().waitFor(); expect(await page.getByTestId("searchTotalCount").innerText()).toEqual(totalSearchCount); }); diff --git a/e2e/specs/taxonomy.spec.ts b/e2e/specs/taxonomy.spec.ts index b4c9ed04f0..69573f8a36 100644 --- a/e2e/specs/taxonomy.spec.ts +++ b/e2e/specs/taxonomy.spec.ts @@ -17,7 +17,7 @@ test.beforeEach(async ({ page }) => { test("should have settingsMenu available after clicking button", async ({ page }) => { await page.getByTestId("structure").getByRole("button", { name: "Engelsk 1" }).click(); await page.getByTestId("settings-button").click(); - expect(await page.getByTestId("settings-menu-modal").count()).toEqual(1); + expect(await page.getByTestId("settings-menu-dialog").count()).toEqual(1); }); test("should be able to change name of node", async ({ page }) => { @@ -42,8 +42,8 @@ test("should be able to change visibility", async ({ page }) => { await page.getByTestId("structure").getByRole("button", { name: "Engelsk 1" }).click(); await page.getByTestId("settings-button").click(); await page.getByTestId("toggleVisibilityButton").click(); - expect(await page.getByLabel("Synlig").count()).toEqual(1); - expect(await page.getByLabel("Synlig").isChecked()).toBeTruthy(); + expect(await page.getByLabel("Synlig", { exact: true }).count()).toEqual(1); + expect(await page.getByLabel("Synlig", { exact: true }).isChecked()).toBeTruthy(); }); test("can toggle favourites", async ({ page }) => { diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..4a7a285f37 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// @ts-check + +import config from "eslint-config-ndla"; +import tseslint from "typescript-eslint"; + +export default tseslint.config(...config, { + ignores: ["**/h5pResizer.ts"], +}); diff --git a/package.json b/package.json index 3e7e5108cc..ec149fea40 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "clean": "node scripts/clean.mjs", "check-all": "yarn lint && yarn tsc --noEmit && yarn test", "lint": "yarn format-check && yarn lint-es", - "lint-es": "eslint --cache --cache-location '.eslintcache/' --ext .js,.jsx,.ts,.tsx --max-warnings=0 src", + "lint-es": "eslint --cache --cache-location '.eslintcache/' --max-warnings=0 src", "format-check": "prettier '{src,scripts}/**/*(*.js|*.jsx|*.ts|*.tsx)' --check", "format": "prettier '{src,scripts}/**/*(*.js|*.jsx|*.ts|*.tsx)' --write", "start:tsc": "tsc -b -w --preserveWatchOutput", @@ -31,49 +31,49 @@ "npm": ">=8.0.0" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@emotion/babel-plugin": "11.11.0", - "@emotion/eslint-plugin": "11.11.0", - "@emotion/jest": "11.11.0", - "@ndla/preset-panda": "^0.0.44", - "@ndla/scripts": "^2.1.2", - "@ndla/types-backend": "^0.2.96", + "@babel/core": "^7.26.0", + "@emotion/jest": "11.13.0", + "@ndla/preset-panda": "^0.0.48", + "@ndla/scripts": "^2.1.3", + "@ndla/types-backend": "^0.2.98", "@ndla/types-embed": "^5.0.4-alpha.0", "@ndla/types-taxonomy": "^1.0.30", - "@pandacss/dev": "^0.46.1", + "@pandacss/dev": "^0.48.0", "@playwright/test": "^1.42.0", - "@tanstack/react-query-devtools": "^5.7.2", + "@tanstack/react-query-devtools": "^5.62.3", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.0.1", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", "@types/auth0-js": "^9.13.2", "@types/diff-match-patch": "^1.0.32", "@types/express": "^5.0.0", + "@types/is-hotkey": "^0.1.10", "@types/lodash": "^4.17.10", "@types/node": "^20.12.12", "@types/node-fetch": "^2.6.2", "@types/prismjs": "^1.26.0", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "concurrently": "^8.2.2", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.1.0", "cross-env": "^7.0.3", - "esbuild": "^0.20.2", - "eslint": "^8.57.0", - "eslint-config-ndla": "^5.0.3", + "esbuild": "^0.24.0", + "eslint": "^9.15.0", + "eslint-config-ndla": "^6.0.0-alpha.0", "filter-console": "^0.1.1", "jsdom": "^25.0.1", "nock": "^13.5.4", - "postcss": "^8.4.39", + "postcss": "^8.4.49", "postcss-import": "^16.1.0", - "postcss-preset-env": "^9.6.0", + "postcss-preset-env": "^10.1.1", "postcss-reporter": "^7.1.0", - "sirv": "^2.0.4", - "tsx": "^4.7.2", - "typescript": "^5.6.3", - "vite": "^5.4.8", - "vitest": "^2.1.1" + "sirv": "^3.0.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0", + "vite": "^6.0.1", + "vitest": "^2.1.6" }, "dependencies": { "@ark-ui/react": "^4.1.2", @@ -81,30 +81,26 @@ "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", - "@emotion/react": "^11.11.1", - "@emotion/styled": "^11.11.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", "@fontsource/source-code-pro": "^4.5.9", "@fontsource/source-sans-pro": "^4.5.9", "@fontsource/source-serif-pro": "^4.5.7", - "@ndla/article-converter": "^10.0.73-alpha.0", - "@ndla/audio-search": "^7.0.60-alpha.0", - "@ndla/button": "^15.0.44-alpha.0", - "@ndla/error-reporter": "^2.0.4", - "@ndla/hooks": "^2.1.9", - "@ndla/icons": "^8.0.43-alpha.0", - "@ndla/image-search": "^11.0.62-alpha.0", - "@ndla/licenses": "^8.0.3-alpha.0", - "@ndla/primitives": "^1.0.54-alpha.0", - "@ndla/safelink": "^7.0.55-alpha.0", - "@ndla/styled-system": "^0.0.27", - "@ndla/tracker": "^5.0.11-alpha.0", - "@ndla/ui": "^56.0.70-alpha.0", - "@ndla/util": "^5.0.0-alpha.0", - "@ndla/video-search": "^8.0.59-alpha.0", - "@radix-ui/react-popover": "^1.0.3", - "@radix-ui/react-portal": "^1.0.3", - "@radix-ui/react-toolbar": "^1.0.4", - "@tanstack/react-query": "5.7.2", + "@ndla/article-converter": "^10.0.81-alpha.0", + "@ndla/audio-search": "^7.0.68-alpha.0", + "@ndla/error-reporter": "^2.0.7", + "@ndla/hooks": "^2.1.11", + "@ndla/icons": "^8.0.48-alpha.0", + "@ndla/image-search": "^11.0.70-alpha.0", + "@ndla/licenses": "^8.0.5-alpha.0", + "@ndla/primitives": "^1.0.62-alpha.0", + "@ndla/safelink": "^7.0.63-alpha.0", + "@ndla/styled-system": "^0.0.29", + "@ndla/tracker": "^5.0.14-alpha.0", + "@ndla/ui": "^56.0.78-alpha.0", + "@ndla/util": "^5.0.3-alpha.0", + "@ndla/video-search": "^8.0.67-alpha.0", + "@tanstack/react-query": "5.62.3", "auth0-js": "^9.22.1", "cheerio": "^1.0.0-rc.12", "compression": "^1.7.4", @@ -121,7 +117,7 @@ "he": "^1.2.0", "helmet": "^3.23.3", "history": "^5.1.0", - "html-react-parser": "^5.1.8", + "html-react-parser": "^5.2.0", "htmldiff-js": "^1.0.5", "i18next": "^23.11.5", "is-hotkey": "^0.2.0", diff --git a/src/components/Accordion/FormAccordion.tsx b/src/components/Accordion/FormAccordion.tsx index 37ca30ef01..8ca5403476 100644 --- a/src/components/Accordion/FormAccordion.tsx +++ b/src/components/Accordion/FormAccordion.tsx @@ -7,7 +7,7 @@ */ import { ReactNode, memo } from "react"; -import { ArrowDownShortLine } from "@ndla/icons/common"; +import { ArrowDownShortLine } from "@ndla/icons"; import { AccordionItem, AccordionItemContent, diff --git a/src/components/Accordion/FormAccordions.tsx b/src/components/Accordion/FormAccordions.tsx index 9a0b77c164..4c98acbcb3 100644 --- a/src/components/Accordion/FormAccordions.tsx +++ b/src/components/Accordion/FormAccordions.tsx @@ -10,7 +10,6 @@ import { ReactElement, memo, useState } from "react"; import { AccordionRoot } from "@ndla/primitives"; import { styled } from "@ndla/styled-system/jsx"; import { FormAccordionProps } from "./FormAccordion"; -import OpenAllButton from "./OpenAllButton"; type ChildType = ReactElement | undefined | false; @@ -29,14 +28,6 @@ const AccordionsWrapper = styled("div", { }, }); -const FlexWrapper = styled("div", { - base: { - display: "flex", - flexDirection: "row", - justifyContent: "flex-end", - }, -}); - const StyledAccordionRoot = styled(AccordionRoot, { base: { width: "100%", @@ -48,13 +39,6 @@ const FormAccordions = ({ defaultOpen, children }: Props) => { return ( - - - | undefined | false; - -interface Props { - defaultOpen: string[]; - children: ChildType | ChildType[]; - article?: IArticle; - taxonomy?: Node[]; - updateNotes?: (art: IUpdatedArticle) => Promise; -} - -const ContentWrapper = styled.div` - display: flex; - flex-direction: column; - gap: ${spacing.small}; -`; - -const FlexWrapper = styled.div` - display: flex; -`; - -const RightFlexWrapper = styled.div` - display: flex; - gap: ${spacing.small}; -`; - -const CommentWrapper = styled.div` - width: 100%; - max-width: ${COMMENT_WIDTH}px; - margin-left: ${SPACING_COMMENT}px; - display: flex; - flex-direction: column; - &[data-hidden="true"] { - visibility: none; - } - &[data-none="true"] { - display: none; - } -`; - -const StyledSwitchRoot = styled(SwitchRoot)` - min-height: 40px; -`; - -const FormControls = styled.div` - display: flex; - width: 100%; - padding-left: ${spacing.small}; - justify-content: flex-end; - &[data-enabled-quality-evaluation="true"] { - justify-content: space-between; - } -`; - -const StyledAccordionRoot = styled(AccordionRoot)` - width: 100%; -`; - -const FormAccordionsWithComments = ({ defaultOpen, children, article, taxonomy, updateNotes }: Props) => { - const { t } = useTranslation(); - const { toggleWideArticles, isWideArticle } = useWideArticle(); - const [revisionMetaField, , revisionMetaHelpers] = useField("revisionMeta"); - - const [openAccordions, setOpenAccordions] = useState(defaultOpen); - const [hideComments, setHideComments] = useLocalStorageBooleanState(STORED_HIDE_COMMENTS); - - const isTopicArticle = article?.articleType === "topic-article"; - const isFrontPageArticle = article?.articleType === "frontpage-article"; - - const disableComments = useMemo( - () => !isTopicArticle && [PUBLISHED, ARCHIVED, UNPUBLISHED].some((s) => s === article?.status.current), - [article?.status, isTopicArticle], - ); - - // Topics are updated from structure edit page - const withoutTopics = taxonomy?.filter((n) => n.nodeType !== "TOPIC"); - - return ( - - - - {!isTopicArticle && !isFrontPageArticle && ( - - )} - - {!!article?.id && isFrontPageArticle && ( - toggleWideArticles(article.id)}> - {t("frontpageArticleForm.isFrontpageArticle.toggleArticle")} - - - - - - )} - - - - - {!disableComments && ( - setHideComments(!hideComments)}> - {t("form.comment.showComments")} - - - - - - )} - - - - setOpenAccordions(details.value)} - lazyMount - unmountOnExit - > - {children} - - - {!hideComments && !disableComments && } - - - - ); -}; - -export default memo(FormAccordionsWithComments); diff --git a/src/components/Accordion/OpenAllButton.tsx b/src/components/Accordion/OpenAllButton.tsx deleted file mode 100644 index ae171848f3..0000000000 --- a/src/components/Accordion/OpenAllButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2024-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { Children, useCallback, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@ndla/primitives"; -import { styled } from "@ndla/styled-system/jsx"; -import { ChildType } from "./FormAccordionsWithComments"; - -interface Props { - openAccordions: string[]; - setOpenAccordions: (values: string[]) => void; - formAccordionChildren: ChildType | ChildType[]; -} - -const StyledButton = styled(Button, { - base: { - alignSelf: "flex-end", - }, -}); - -const OpenAllButton = ({ openAccordions, setOpenAccordions, formAccordionChildren }: Props) => { - const { t } = useTranslation(); - - const accordionChildren = useMemo( - () => Children.map(formAccordionChildren, (c) => (c ? c.props?.id : false))?.filter(Boolean) ?? [], - [formAccordionChildren], - ); - - const allOpen = useMemo( - () => accordionChildren.length === openAccordions.length, - [accordionChildren.length, openAccordions.length], - ); - - const onChangeAll = useCallback(() => { - if (allOpen) { - setOpenAccordions([]); - } else { - setOpenAccordions(accordionChildren); - } - }, [allOpen, setOpenAccordions, accordionChildren]); - - return ( - - {allOpen ? t("accordion.closeAll") : t("accordion.openAll")} - - ); -}; - -export default OpenAllButton; diff --git a/src/components/AlertDialog/AlertDialog.tsx b/src/components/AlertDialog/AlertDialog.tsx index 2ebc98bbc6..283619ac9b 100644 --- a/src/components/AlertDialog/AlertDialog.tsx +++ b/src/components/AlertDialog/AlertDialog.tsx @@ -9,7 +9,7 @@ import { ReactNode, useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Portal } from "@ark-ui/react"; -import { ErrorWarningLine } from "@ndla/icons/common"; +import { ErrorWarningLine } from "@ndla/icons"; import { DialogRoot, DialogContent, @@ -20,7 +20,6 @@ import { DialogHeader, DialogFooter, } from "@ndla/primitives"; -import { styled } from "@ndla/styled-system/jsx"; import { MessageSeverity } from "../../interfaces"; import { DialogCloseButton } from "../DialogCloseButton"; diff --git a/src/components/ControlledImageSearchAndUploader.tsx b/src/components/ControlledImageSearchAndUploader.tsx index e9a92b298d..60dcd82d0e 100644 --- a/src/components/ControlledImageSearchAndUploader.tsx +++ b/src/components/ControlledImageSearchAndUploader.tsx @@ -8,8 +8,18 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { ErrorWarningLine } from "@ndla/icons"; import { ImageSearch } from "@ndla/image-search"; -import { Button, TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger, Text } from "@ndla/primitives"; +import { + Button, + MessageBox, + TabsContent, + TabsIndicator, + TabsList, + TabsRoot, + TabsTrigger, + Text, +} from "@ndla/primitives"; import { styled } from "@ndla/styled-system/jsx"; import { IImageMetaInformationV3, @@ -19,7 +29,6 @@ import { ISearchParams, } from "@ndla/types-backend/image-api"; import { useImageSearchTranslations } from "@ndla/ui"; -import EditorErrorMessage from "./SlateEditor/EditorErrorMessage"; import ImageForm from "../containers/ImageUploader/components/ImageForm"; import { draftLicensesToImageLicenses } from "../modules/draft/draftApiUtils"; import { useLicenses } from "../modules/draft/draftQueries"; @@ -135,7 +144,10 @@ const ImageSearchAndUploader = ({ supportedLanguages={image?.supportedLanguages ?? [locale]} /> ) : ( - + + + {t("errorMessage.description")} + )} diff --git a/src/components/DatePicker.tsx b/src/components/DatePicker.tsx index 6c5b3b3e09..b106f9d241 100644 --- a/src/components/DatePicker.tsx +++ b/src/components/DatePicker.tsx @@ -57,7 +57,7 @@ const DatePickerDropdown = (props: DropdownProps) => { return ; } const { children, ...rest } = props; - //@ts-ignore + // @ts-expect-error - TODO: We're going to migrate away from this at some point. return ; }; diff --git a/src/components/DeleteButton.tsx b/src/components/DeleteButton.tsx index b290602aa6..0bcd361e50 100644 --- a/src/components/DeleteButton.tsx +++ b/src/components/DeleteButton.tsx @@ -6,7 +6,7 @@ * */ -import { DeleteBinLine } from "@ndla/icons/action"; +import { DeleteBinLine } from "@ndla/icons"; import { IconButton, type IconButtonProps } from "@ndla/primitives"; export const DeleteButton = ({ children, ...rest }: IconButtonProps) => ( diff --git a/src/components/DialogCloseButton.tsx b/src/components/DialogCloseButton.tsx index 0b44509bde..b663ece91e 100644 --- a/src/components/DialogCloseButton.tsx +++ b/src/components/DialogCloseButton.tsx @@ -8,7 +8,7 @@ import { forwardRef } from "react"; import { useTranslation } from "react-i18next"; -import { CloseLine } from "@ndla/icons/action"; +import { CloseLine } from "@ndla/icons"; import { DialogCloseTrigger, IconButton, IconButtonProps } from "@ndla/primitives"; export const DialogCloseButton = forwardRef( diff --git a/src/components/DisplayEmbed/helpers/h5pResizer.ts b/src/components/DisplayEmbed/helpers/h5pResizer.ts index 03283ea101..0d39251167 100644 --- a/src/components/DisplayEmbed/helpers/h5pResizer.ts +++ b/src/components/DisplayEmbed/helpers/h5pResizer.ts @@ -97,4 +97,23 @@ iframes[i].contentWindow.postMessage(ready, "*"); } } + + // Handle resizing in Edlib 3 + window.addEventListener("message", (event) => { + if (event?.data?.context) { + return; // not Edlib 3 + } + + if (event?.data?.action !== "resize" || !event?.data?.scrollHeight) { + return; // not a resize event + } + + const iframe = [...document.getElementsByTagName("iframe")].find((frame) => frame.contentWindow === event.source); + + if (!iframe) { + return; + } + + iframe.height = String(event.data.scrollHeight + iframe.getBoundingClientRect().height - iframe.scrollHeight); + }); })(); diff --git a/src/components/DraggableItem.tsx b/src/components/DraggableItem.tsx index f417e46d22..96b35341df 100644 --- a/src/components/DraggableItem.tsx +++ b/src/components/DraggableItem.tsx @@ -23,6 +23,7 @@ interface Props { const StyledListElement = styled("li", { base: { + position: "relative", listStyle: "none", display: "flex", alignItems: "center", @@ -30,10 +31,17 @@ const StyledListElement = styled("li", { cursor: "grab", }, }, + variants: { + isDragging: { + true: { + zIndex: "docked", + }, + }, + }, }); const DraggableItem = ({ id, index, children, dragHandle, disabled }: Props) => { - const { attributes, setNodeRef, transform, transition, listeners, setActivatorNodeRef } = useSortable({ + const { attributes, setNodeRef, transform, transition, listeners, setActivatorNodeRef, isDragging } = useSortable({ id: id, disabled, data: { @@ -60,6 +68,7 @@ const DraggableItem = ({ id, index, children, dragHandle, disabled }: Props) => ref={setNodeRef} data-has-handle={!!dragHandle} style={style} + isDragging={isDragging} {...attributes} {...(dragHandle ? {} : listeners)} > diff --git a/src/components/EditMarkupLink.tsx b/src/components/EditMarkupLink.tsx index bfc07981e3..3204633980 100644 --- a/src/components/EditMarkupLink.tsx +++ b/src/components/EditMarkupLink.tsx @@ -7,7 +7,7 @@ */ import { useLocation } from "react-router-dom"; -import { CodeView } from "@ndla/icons/editor"; +import { CodeView } from "@ndla/icons"; import { SafeLinkIconButton } from "@ndla/safelink"; interface Props { diff --git a/src/components/Field/FieldHeader.tsx b/src/components/Field/FieldHeader.tsx deleted file mode 100644 index 52e0edea56..0000000000 --- a/src/components/Field/FieldHeader.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2018-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { ReactNode } from "react"; -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; -import { colors, spacing, fonts } from "@ndla/core"; - -interface props { - title: string; - subTitle?: string; - width?: number; - children?: ReactNode; -} - -interface StyledWrapperProps { - wrapperWidth: number; -} - -const StyledWrapper = styled.div` - align-items: center; - border-bottom: 2px solid ${colors.brand.light}; - display: flex; - padding-bottom: ${spacing.xsmall}; - padding-top: ${spacing.normal}; - ${(props) => css` - width: ${props.wrapperWidth}%; - `}; - > div { - align-items: center; - display: flex; - flex-grow: 1; - justify-content: flex-end; - } - button { - background: 0; - border: 0; - margin: 0; - padding: 0; - } -`; - -const StyledTitle = styled.h2` - color: ${colors.text.primary}; - font-weight: ${fonts.weight.bold}; - margin: 0; - padding-right: ${spacing.small}; - text-transform: uppercase; - ${fonts.sizes(20, 1.1)}; - span { - ${fonts.sizes(16, 1.1)}; - color: ${colors.text.light}; - font-weight: ${fonts.weight.normal}; - padding-left: ${spacing.small}; - text-transform: none; - } -`; - -const FieldHeader = ({ title, subTitle, width = 1, children }: props) => ( - - - {title} - {subTitle && {subTitle}} - -
{children}
-
-); - -export default FieldHeader; diff --git a/src/components/FileUploader/FileUploader.tsx b/src/components/FileUploader/FileUploader.tsx index 296228535a..927d85226a 100644 --- a/src/components/FileUploader/FileUploader.tsx +++ b/src/components/FileUploader/FileUploader.tsx @@ -9,9 +9,7 @@ import { Formik } from "formik"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { DeleteBinLine } from "@ndla/icons/action"; -import { FileDocumentOutline } from "@ndla/icons/common"; -import { UploadCloudLine } from "@ndla/icons/editor"; +import { DeleteBinLine, FileTextLine, UploadCloudLine } from "@ndla/icons"; import { Button, FieldErrorMessage, @@ -157,7 +155,7 @@ const FileUploader = ({ onFileSave, close }: Props) => { acceptedFiles.map((file, index) => ( - + @@ -185,7 +183,7 @@ const FileUploader = ({ onFileSave, close }: Props) => { {t("form.save")} - {errorMessage && {errorMessage}} + {!!errorMessage && {errorMessage}} )} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000000..5d8eeab75e --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2016-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { createListCollection } from "@ark-ui/react"; +import { PageContent, SelectContent, SelectLabel, SelectRoot, SelectValueText, Text } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; +import { supportedLanguages } from "../i18n2"; +import { LocaleType } from "../interfaces"; +import { GenericSelectItem, GenericSelectTrigger } from "./abstractions/Select"; + +export const FooterBlock = styled("footer", { + base: { + position: "relative", + background: "primary", + paddingBlock: "medium", + }, +}); + +const StyledGenericSelectTrigger = styled(GenericSelectTrigger, { + base: { + width: "unset", + }, +}); + +const FooterContainer = styled("div", { + base: { + marginBlockStart: "medium", + }, +}); + +const LanguageSelectorWrapper = styled("div", { + base: { + display: "flex", + justifyContent: "center", + }, +}); + +const FooterTextWrapper = styled("div", { + base: { + alignSelf: "flex-end", + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + }, +}); + +const FooterContent = styled("div", { + base: { + display: "flex", + gap: "xsmall", + alignItems: "flex-end", + justifyContent: "flex-end", + }, +}); + +export const Footer = () => { + const { t, i18n } = useTranslation(); + + const supportedLanguagesCollection = useMemo( + () => + createListCollection({ + items: supportedLanguages, + itemToString: (item) => t(`languages.${item}`), + }), + [t], + ); + + return ( + + + + + + i18n.changeLanguage(details.value[0] as LocaleType)} + value={[i18n.language]} + > + {t("languages.prefixChangeLanguage")} + + {t("languages.prefixChangeLanguage")} + + + {supportedLanguages.map((lang) => ( + + {t(`languages.${lang}`)} + + ))} + + + + + {t("footer.info")} + + {t("footer.editorInChief")} Sigurd Trageton + + + + + + + ); +}; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx deleted file mode 100644 index 3ef2cb07b5..0000000000 --- a/src/components/Footer/Footer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2019-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { ReactNode } from "react"; -import styled from "@emotion/styled"; -import { colors, spacing, stackOrder } from "@ndla/core"; -import { MAX_PAGE_WIDTH } from "../../constants"; -import { MAX_WIDTH_WITH_COMMENTS } from "../../containers/ArticlePage/styles"; - -const StyledFooter = styled.footer` - position: fixed; - bottom: 0; - left: -10px; - right: -10px; - background: #fff; - z-index: ${stackOrder.banner}; - box-shadow: -10px 0 10px rgba(0, 0, 0, 0.4); - display: flex; - justify-content: center; -`; - -const StyledContentWrapper = styled.div` - max-width: ${MAX_PAGE_WIDTH}px; - width: 100%; - padding: ${spacing.small}; - display: flex; - justify-content: space-between; - gap: ${spacing.medium}; - &[data-article="true"] { - max-width: ${MAX_WIDTH_WITH_COMMENTS}px; - } - > div { - display: flex; - align-items: center; - > hr { - width: 1px; - height: ${spacing.medium}; - background: ${colors.brand.greyLight}; - margin: 0 ${spacing.normal} 0 ${spacing.small}; - &:before { - content: none; - } - } - } -`; - -type Props = { - children: ReactNode; - className?: string; - isArticle?: boolean; -}; - -const Footer = ({ children, className, isArticle }: Props) => ( - - - {children} - - -); - -export default Footer; diff --git a/src/components/Form/ContentEditableFieldLabel.tsx b/src/components/Form/ContentEditableFieldLabel.tsx new file mode 100644 index 0000000000..5dd9a3fbb6 --- /dev/null +++ b/src/components/Form/ContentEditableFieldLabel.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useRef } from "react"; +import { useFieldContext } from "@ark-ui/react"; +import { mergeProps } from "@zag-js/react"; +import { Label, LabelProps } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; + +interface Props extends LabelProps {} + +const StyledLabel = styled(Label, { + base: { + cursor: "default", + }, +}); + +export const ContentEditableFieldLabel = ({ children, ...props }: Props) => { + const field = useFieldContext(); + const { htmlFor, ...rest } = mergeProps(field?.getLabelProps(), props); + const ref = useRef(null); + + return ( + { + document.getElementById(field?.ids.control)?.focus(); + }} + {...(rest as LabelProps)} + ref={ref} + > +

{children}

+
+ ); +}; diff --git a/src/components/Form/FieldWarning.tsx b/src/components/Form/FieldWarning.tsx new file mode 100644 index 0000000000..b3aff94577 --- /dev/null +++ b/src/components/Form/FieldWarning.tsx @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useFormikContext } from "formik"; +import { ComponentPropsWithRef, forwardRef } from "react"; +import { FieldHelper, TextProps } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; + +interface Props extends ComponentPropsWithRef<"div"> { + name: string; +} + +const StyledFieldHelper = styled(FieldHelper, { + base: { + // TODO: Replace this + color: "#8c8c00", + }, +}); + +export const FieldWarning = forwardRef(({ name, ...props }, ref) => { + const { status } = useFormikContext(); + return ( + + {status?.warnings?.[name]} + + ); +}); diff --git a/src/components/Form/FormRemainingCharacters.tsx b/src/components/Form/FormRemainingCharacters.tsx index bbfcbcb9a2..b4e3dea891 100644 --- a/src/components/Form/FormRemainingCharacters.tsx +++ b/src/components/Form/FormRemainingCharacters.tsx @@ -9,7 +9,7 @@ import { ComponentPropsWithRef, forwardRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Descendant, Node } from "slate"; -import { FieldHelper } from "@ndla/primitives"; +import { FieldHelper, Text, TextProps } from "@ndla/primitives"; import useDebounce from "../../util/useDebounce"; interface Props extends ComponentPropsWithRef<"div"> { @@ -23,8 +23,8 @@ const getValueLength = (value: string | Descendant[]) => ? value.map((node) => Node.string(node)).reduce((acc, curr) => acc + curr, "").length : value.length; -export const FormRemainingCharacters = forwardRef( - ({ value, maxLength, debounceDuration = 500, ...rest }, ref) => { +export const FormRemainingCharacters = forwardRef( + ({ value, textStyle = "label.xsmall", maxLength, debounceDuration = 500, ...rest }, ref) => { const { t } = useTranslation(); const debouncedValue = useDebounce(value, debounceDuration); const valueLength = getValueLength(value); @@ -33,9 +33,9 @@ export const FormRemainingCharacters = forwardRef( return ( <> -
+ {t("form.remainingCharacters", { remaining: maxLength - valueLength, maxLength })} -
+ {t("form.remainingCharacters", { remaining: maxLength - debouncedValueLength, maxLength })} diff --git a/src/components/Form/GenericSearchCombobox.tsx b/src/components/Form/GenericSearchCombobox.tsx index 4b035783bc..f01e38ec2b 100644 --- a/src/components/Form/GenericSearchCombobox.tsx +++ b/src/components/Form/GenericSearchCombobox.tsx @@ -14,7 +14,6 @@ import { ComboboxContent, ComboboxItem, ComboboxList, - ComboboxPositioner, ComboboxRoot, ComboboxRootProps, PaginationRootProps, @@ -33,14 +32,12 @@ interface PaginationData { const StyledComboboxContent = styled(ComboboxContent, { base: { - maxHeight: "unset", overflowY: "unset", }, }); const StyledComboboxList = styled(ComboboxList, { base: { - maxHeight: "surface.xsmall", overflowY: "auto", }, }); @@ -98,27 +95,25 @@ export const GenericSearchCombobox = ({ {...props} > {children} - - - - {collection.items.map((item) => ( - - {renderItem(item)} - - ))} - - {isSuccess && {t("dropdown.numberHits", { hits: paginationData?.totalCount ?? 0 })}} - {!!paginationData && paginationData.totalCount > paginationData.pageSize && ( - - )} - - + + + {collection.items.map((item) => ( + + {renderItem(item)} + + ))} + + {!!isSuccess && {t("dropdown.numberHits", { hits: paginationData?.totalCount ?? 0 })}} + {!!paginationData && paginationData.totalCount > paginationData.pageSize && ( + + )} + ); }; diff --git a/src/components/Form/ListResource.tsx b/src/components/Form/ListResource.tsx index 7dda7fbdba..61a92fec94 100644 --- a/src/components/Form/ListResource.tsx +++ b/src/components/Form/ListResource.tsx @@ -8,8 +8,7 @@ import { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { DeleteBinLine } from "@ndla/icons/action"; -import { ImageLine } from "@ndla/icons/editor"; +import { DeleteBinLine, ImageLine } from "@ndla/icons"; import { IconButton, ListItemContent, ListItemHeading, ListItemImage, ListItemRoot } from "@ndla/primitives"; import { SafeLink } from "@ndla/safelink"; import { styled } from "@ndla/styled-system/jsx"; @@ -68,7 +67,7 @@ const ListResource = ({ {title} - {onDelete && ( + {!!onDelete && ( void; + search?: () => void; +} + +const SearchControlButtons = ({ reset, search }: Props) => { + const { t } = useTranslation(); + return ( + + + {t("searchForm.empty")} + + {search ? ( + + {t("searchForm.btn")} + + ) : ( + + {t("searchForm.btn")} + + )} + + ); +}; + +export default SearchControlButtons; diff --git a/src/components/Form/SearchHeader.tsx b/src/components/Form/SearchHeader.tsx new file mode 100644 index 0000000000..b03bef0a99 --- /dev/null +++ b/src/components/Form/SearchHeader.tsx @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useTranslation } from "react-i18next"; +import { Heading } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; +import { IUserData } from "@ndla/types-backend/draft-api"; +import SearchSaveButton from "./SearchSaveButton"; +import { Filters } from "./SearchTagGroup"; +import { SearchType } from "../../interfaces"; + +const StyledSearchHeader = styled("div", { + base: { + display: "flex", + gap: "3xsmall", + alignItems: "flex-end", + justifyContent: "space-between", + }, +}); + +interface Props { + type: SearchType; + filters?: Filters; + userData: IUserData | undefined; +} + +const SearchHeader = ({ type, filters, userData }: Props) => { + const { t } = useTranslation(); + + return ( + + {t(`searchPage.header.${type}`)} + {!!filters && } + + ); +}; + +export default SearchHeader; diff --git a/src/components/Form/SearchSaveButton.tsx b/src/components/Form/SearchSaveButton.tsx new file mode 100644 index 0000000000..dae6f01441 --- /dev/null +++ b/src/components/Form/SearchSaveButton.tsx @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2021-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { TFunction } from "i18next"; +import { parse, stringify } from "query-string"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, Text } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; +import { IUserData } from "@ndla/types-backend/draft-api"; +import { Filters } from "../../components/Form/SearchTagGroup"; +import SaveButton from "../../components/SaveButton"; +import { SearchType } from "../../interfaces"; +import { useUpdateUserDataMutation } from "../../modules/draft/draftQueries"; + +type Error = "alreadyExist" | "other" | "fetchFailed" | ""; + +const ButtonWrapper = styled("div", { + base: { + display: "flex", + gap: "3xsmall", + }, +}); + +const StyledWrapper = styled("div", { + base: { + display: "flex", + flexDirection: "column", + gap: "3xsmall", + alignItems: "flex-end", + }, +}); + +const getSavedSearchRelativeUrl = (inputValue: string) => { + const relativeUrl = inputValue.split("search")[1]; + return "/search".concat(relativeUrl); +}; + +const createSearchPhrase = (filters: Filters, searchContentType: SearchType, t: TFunction): string => { + const activeFilters = Object.entries(filters) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => t(`searchForm.tagType.${key}`, { value })); + const contentTypePhrase = t(`searchTypes.${searchContentType}`); + if (!activeFilters.length) return contentTypePhrase; + return `${contentTypePhrase}, ${activeFilters.join(", ")}`; +}; + +const createSearchString = (location: Location) => { + const searchObject = parse(location.search); + if (searchObject.page) { + delete searchObject.page; + } + return location.pathname + "?" + stringify(searchObject); +}; + +interface Props { + filters: Filters; + searchContentType: SearchType; + userData?: IUserData | undefined; +} + +const SearchSaveButton = ({ filters, searchContentType, userData }: Props) => { + const { t } = useTranslation(); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const { mutateAsync } = useUpdateUserDataMutation(); + + const savedSearches = userData?.savedSearches ?? []; + + useEffect(() => { + setError(""); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [window.location.search]); + + const handleSuccess = () => { + setSuccess(true); + setLoading(false); + setTimeout(() => setSuccess(false), 2500); + }; + + const deleteSearch = (index: number) => { + const reduced_array = userData?.savedSearches?.filter((_, idx) => idx !== index); + mutateAsync({ savedSearches: reduced_array }); + }; + + const handleFailure = (type: Error) => { + setLoading(false); + setError(type); + setSuccess(false); + }; + + const saveSearch = async () => { + setError(""); + setLoading(true); + const oldSearchList = savedSearches; + if (!oldSearchList) { + handleFailure("fetchFailed"); + return; + } + const newSearch = getSavedSearchRelativeUrl(createSearchString(window.location)); + const newSearchPhrase = createSearchPhrase(filters, searchContentType, t); + + const newSearchList = [{ searchUrl: newSearch, searchPhrase: newSearchPhrase }, ...oldSearchList]; + + if (!oldSearchList.find((s) => s.searchUrl === newSearch)) { + mutateAsync({ savedSearches: newSearchList }) + .then(() => handleSuccess()) + .catch(() => handleFailure("other")); + } else { + handleFailure("alreadyExist"); + } + }; + + const currentSearch = getSavedSearchRelativeUrl(createSearchString(window.location)); + const isSaved = savedSearches.some((s) => s.searchUrl === getSavedSearchRelativeUrl(currentSearch)); + + return ( + + {!!isSaved && ( + + )} + + + {!!error && {t("searchPage.save." + error)}} + + + ); +}; + +export default SearchSaveButton; diff --git a/src/components/Form/SearchTagGroup.tsx b/src/components/Form/SearchTagGroup.tsx new file mode 100644 index 0000000000..db1e6aed36 --- /dev/null +++ b/src/components/Form/SearchTagGroup.tsx @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2016-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useId } from "react"; +import { useTranslation } from "react-i18next"; +import { CloseLine } from "@ndla/icons"; +import { Text, Button } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; +import { visuallyHidden } from "@ndla/styled-system/patterns"; +import { SearchParams } from "../../interfaces"; + +const TagsWrapper = styled("div", { + base: { + display: "flex", + gap: "3xsmall", + flexWrap: "wrap", + }, +}); + +export type Filters = { [key in keyof SearchParams]: string | undefined }; +interface Props { + tags: Filters; + onRemoveTag: (parameterName: keyof SearchParams, value?: string) => void; +} + +const SearchTagGroup = ({ tags, onRemoveTag }: Props) => { + const { t } = useTranslation(); + const activeFiltersId = useId(); + return ( + <> + + {t("searchPage.activeFilters")} + + + {Object.entries(tags).map(([key, value]) => { + if (!value) return null; + return ( + + ); + })} + + + ); +}; + +export default SearchTagGroup; diff --git a/src/components/Form/SearchTagsContent.tsx b/src/components/Form/SearchTagsContent.tsx index 0c0f86a4c0..0dc28af82b 100644 --- a/src/components/Form/SearchTagsContent.tsx +++ b/src/components/Form/SearchTagsContent.tsx @@ -8,7 +8,7 @@ import { forwardRef } from "react"; import { useTranslation } from "react-i18next"; -import { ComboboxContent, ComboboxContentProps, ComboboxPositioner, Spinner, Text } from "@ndla/primitives"; +import { ComboboxContent, ComboboxContentProps, Spinner, Text } from "@ndla/primitives"; interface Props extends ComboboxContentProps { isFetching: boolean; @@ -18,10 +18,8 @@ interface Props extends ComboboxContentProps { export const SearchTagsContent = forwardRef(({ isFetching, hits, children, ...props }, ref) => { const { t } = useTranslation(); return ( - - - {isFetching ? : hits ? children : {t("dropdown.numberHits", { hits: 0 })}} - - + + {isFetching ? : hits ? children : {t("dropdown.numberHits", { hits: 0 })}} + ); }); diff --git a/src/components/Form/SearchTagsTagSelectorInput.tsx b/src/components/Form/SearchTagsTagSelectorInput.tsx index d8c606f898..a2f41a99ed 100644 --- a/src/components/Form/SearchTagsTagSelectorInput.tsx +++ b/src/components/Form/SearchTagsTagSelectorInput.tsx @@ -7,8 +7,7 @@ */ import { forwardRef } from "react"; -import { CloseLine } from "@ndla/icons/action"; -import { ArrowDownShortLine } from "@ndla/icons/common"; +import { CloseLine, ArrowDownShortLine } from "@ndla/icons"; import { IconButton, InputContainer } from "@ndla/primitives"; import { HStack } from "@ndla/styled-system/jsx"; import { diff --git a/src/components/Form/SegmentHeader.tsx b/src/components/Form/SegmentHeader.tsx new file mode 100644 index 0000000000..8d49adec93 --- /dev/null +++ b/src/components/Form/SegmentHeader.tsx @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { ComponentPropsWithoutRef } from "react"; +import { styled } from "@ndla/styled-system/jsx"; + +const StyledSegmentHeader = styled("div", { + base: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + borderBlockEnd: "2px solid", + borderColor: "stroke.default", + }, +}); + +export const SegmentHeader = ({ children, ...rest }: ComponentPropsWithoutRef<"div">) => ( + {children} +); diff --git a/src/components/Form/SelectRenderer.tsx b/src/components/Form/SelectRenderer.tsx new file mode 100644 index 0000000000..f33d787524 --- /dev/null +++ b/src/components/Form/SelectRenderer.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useTranslation } from "react-i18next"; +import { OnFieldChangeFunction, SearchParams } from "../../interfaces"; +import ObjectSelector, { SelectOption } from "../ObjectSelector"; + +export type SelectElement = { + name: keyof SearchParams; + options: SelectOption[]; + value?: string; +}; + +interface Props { + selectElements: SelectElement[]; + searchObject: SearchParams; + onFieldChange: OnFieldChangeFunction; +} + +export const SelectRenderer = ({ selectElements, searchObject, onFieldChange }: Props) => { + const { t } = useTranslation(); + return ( + <> + {selectElements.map((selectElement) => ( + onFieldChange(selectElement.name, value)} + /> + ))} + + ); +}; diff --git a/src/components/Form/utils.ts b/src/components/Form/utils.ts index acf0a694a2..f2e77ddf78 100644 --- a/src/components/Form/utils.ts +++ b/src/components/Form/utils.ts @@ -11,7 +11,11 @@ import { RefObject } from "react"; // keyboard scrolling does not work properly when items are not nested directly within // ComboboxContent, so we need to provide a custom scroll function // TODO: Check if ark provides a better fix for this. -export const scrollToIndexFn = (contentRef: RefObject, index: number) => { +export const scrollToIndexFn = (contentRef: RefObject, index: number) => { const el = contentRef.current?.querySelectorAll(`[role='option']`)[index]; el?.scrollIntoView({ behavior: "auto", block: "nearest" }); }; + +export const getTagName = (id: string | undefined, data: { id: string; name: string }[] = []) => { + return id ? data.find((entry) => entry.id === id)?.name : undefined; +}; diff --git a/src/components/FormWrapper.tsx b/src/components/FormWrapper.tsx index f30c59dcc0..68eb9d2e4f 100644 --- a/src/components/FormWrapper.tsx +++ b/src/components/FormWrapper.tsx @@ -6,22 +6,26 @@ * */ -import { Form } from "formik"; -import { ReactNode } from "react"; -import StyledForm from "./StyledFormComponents"; +import { ReactNode, ComponentPropsWithRef } from "react"; +import { styled } from "@ndla/styled-system/jsx"; +import { Form } from "./FormikForm"; -interface Props { +interface Props extends ComponentPropsWithRef<"form"> { inModal?: boolean; children: ReactNode; } -const DivForm = StyledForm.withComponent("div"); +const StyledForm = styled(Form, { + base: { + width: "100%", + }, +}); -const FormWrapper = ({ inModal, children }: Props) => { +const FormWrapper = ({ inModal, children, ...rest }: Props) => { if (inModal) { - return {children}; + return {children}; } - return
{children}
; + return
{children}
; }; export default FormWrapper; diff --git a/src/components/FormikField/FormikField.tsx b/src/components/FormikField/FormikField.tsx deleted file mode 100644 index fe8fcf4cec..0000000000 --- a/src/components/FormikField/FormikField.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2019-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { Field, FieldAttributes, FormikValues, FieldProps, useFormikContext } from "formik"; -import get from "lodash/get"; -import { ReactElement, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { Node } from "slate"; -import styled from "@emotion/styled"; -import { spacing } from "@ndla/core"; -import FormikFieldDescription from "./FormikFieldDescription"; -import FormikFieldHelp from "./FormikFieldHelp"; -import FormikFieldLabel from "./FormikFieldLabel"; -import FormikRemainingCharacters from "./FormikRemainingCharacters"; -interface StyledFieldProps { - right?: boolean; - isTitle?: boolean; - noBorder?: boolean; -} - -const StyledField = styled.div` - margin-top: 2rem; - position: relative; - & > select { - width: 100%; - display: block; - } - & label { - font-size: 1.5rem; - } - &[data-no-border="true"] { - & input { - border: none; - padding: 0; - margin: 0; - outline: none; - } - } - &[data-right="true"] { - text-align: right; - margin-right: ${spacing.small}; - } - &[data-is-title="true"] { - & input { - font-size: 2.11111rem; - } - & div { - font-size: 2.11111rem; - } - } -`; - -const StyledErrorPreLine = styled.span` - white-space: pre-line; -`; - -interface Props { - noBorder?: boolean; - right?: boolean; - title?: boolean; - name: string; - label?: string; - showError?: boolean; - obligatory?: boolean; - description?: string; - maxLength?: number; - showMaxLength?: boolean; - className?: string; - children?: (props: FieldProps & FieldAttributes) => ReactElement; - placeholder?: string; - type?: string; - autoFocus?: boolean; -} - -const FormikField = ({ - children, - className, - label, - name, - maxLength, - showMaxLength = false, - noBorder = false, - title = false, - right = false, - description, - obligatory, - showError = true, - ...rest -}: Props) => { - const { values, handleBlur, errors, status } = useFormikContext(); - const { t } = useTranslation(); - const isSlateValue = Node.isNodeList(values[name]); - const fieldActions: FieldAttributes = !isSlateValue - ? { - onBlur: (evt: Event) => { - handleBlur(evt); - }, - } - : {}; - - const getRemainingLabel = useCallback( - (maxLength: number, remaining: number) => { - return t("form.remainingCharacters", { maxLength, remaining }); - }, - [t], - ); - - return ( - - - - - {children - ? (formikProps: FormikValues) => { - return children({ - ...formikProps, - field: { - ...formikProps.field, - ...fieldActions, - }, - }); - } - : null} - - {showMaxLength && maxLength && ( - Node.string(node)) - .reduce((str: string, acc: string) => (acc = acc + str), "") - : values[name] - } - /> - )} - {showError && get(errors, name) && ( - - {get(errors, name) as string} - - )} - {status && status["warnings"] && ( - - {get(status.warnings, name)} - - )} - - ); -}; - -export default FormikField; diff --git a/src/components/FormikField/FormikFieldDescription.tsx b/src/components/FormikField/FormikFieldDescription.tsx deleted file mode 100644 index 2bf99742ec..0000000000 --- a/src/components/FormikField/FormikFieldDescription.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2019-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; - -const StyledFormikDescriptionBlock = styled.span` - display: flex; -`; - -const obligatoryDescriptionStyle = css` - background-color: rgba(230, 132, 154, 1); - padding: 0.2em 0.6em; -`; - -const StyledFormikDescription = styled.p` - margin: 0.2em 0; - font-size: 0.75em; - ${(p: Props) => (p.obligatory ? obligatoryDescriptionStyle : "")}; -`; - -interface Props { - description?: string; - obligatory?: boolean; -} - -const FormikFieldDescription = ({ description, obligatory }: Props) => { - if (!description) { - return null; - } - return ( - - {description} - - ); -}; - -export default FormikFieldDescription; diff --git a/src/components/FormikField/FormikFieldHelp.tsx b/src/components/FormikField/FormikFieldHelp.tsx deleted file mode 100644 index 95e52b8ec1..0000000000 --- a/src/components/FormikField/FormikFieldHelp.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2019-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { ReactNode } from "react"; -import styled from "@emotion/styled"; -import { colors, fonts } from "@ndla/core"; - -interface Props { - error?: boolean; - warning?: boolean; - float?: "left" | "right" | "none" | "inherit"; - children: ReactNode; -} - -export const StyledHelpMessage = styled.span` - display: block; - font-size: ${fonts.sizes(14, 1.2)}; - color: ${(p) => (p.error ? colors.support.red : "black")}; - float: ${(p) => p.float || "none"}; -`; - -const StyledWarningMessage = styled.span` - display: block; - font-size: ${fonts.sizes(14, 1.2)}; - color: ${(p) => (p.warning ? "#8c8c00" : "black")}; - float: ${(p) => p.float || "none"}; -`; - -const FormikFieldHelp = ({ error, warning, float, children }: Props) => - warning ? ( - - {children} - - ) : ( - - {children} - - ); - -export default FormikFieldHelp; diff --git a/src/components/FormikField/FormikFieldLabel.tsx b/src/components/FormikField/FormikFieldLabel.tsx deleted file mode 100644 index 3638ebf8c8..0000000000 --- a/src/components/FormikField/FormikFieldLabel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2019-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import styled from "@emotion/styled"; - -interface Props { - name: string; - label?: string; - noBorder?: boolean; -} - -const StyledLabel = styled.label` - display: none !important; -`; - -const FormikFieldLabel = ({ label, noBorder, name }: Props) => { - if (!label) { - return null; - } - if (!noBorder) { - return ; - } - return {label}; -}; - -export default FormikFieldLabel; diff --git a/src/components/FormikField/FormikRemainingCharacters.tsx b/src/components/FormikField/FormikRemainingCharacters.tsx deleted file mode 100644 index 1923950292..0000000000 --- a/src/components/FormikField/FormikRemainingCharacters.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2019-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { ReactNode } from "react"; -import FormikFieldHelp from "./FormikFieldHelp"; - -interface Props { - value: string; - maxLength: number; - getRemainingLabel: (maxLength: number, remaining: number) => ReactNode; -} - -export const FormikRemainingCharacters = ({ value, maxLength, getRemainingLabel }: Props) => { - const currentLength = value ? value.length : 0; - return {getRemainingLabel(maxLength, maxLength - currentLength)}; -}; - -export default FormikRemainingCharacters; diff --git a/src/components/FormikField/index.ts b/src/components/FormikField/index.ts deleted file mode 100644 index 11a299eedf..0000000000 --- a/src/components/FormikField/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2019-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import FormikField from "./FormikField"; -import FormikFieldHelp from "./FormikFieldHelp"; - -export { FormikFieldHelp }; - -export default FormikField; diff --git a/src/components/H5PElement/H5PElement.tsx b/src/components/H5PElement/H5PElement.tsx index 127d1d30f4..795bea745b 100644 --- a/src/components/H5PElement/H5PElement.tsx +++ b/src/components/H5PElement/H5PElement.tsx @@ -8,21 +8,25 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import styled from "@emotion/styled"; +import { styled } from "@ndla/styled-system/jsx"; import { ErrorMessage } from "@ndla/ui"; import { fetchH5PiframeUrl, editH5PiframeUrl, fetchH5PInfo } from "./h5pApi"; import handleError from "../../util/handleError"; -const FlexWrapper = styled.div` - display: flex; - flex: 1; - width: 100%; -`; +const FlexWrapper = styled("div", { + base: { + display: "flex", + flex: "1", + width: "100%", + }, +}); -const StyledIFrame = styled.iframe` - flex: 1; - overflow: hidden; -`; +const StyledIFrame = styled("iframe", { + base: { + flex: "1", + overflow: "hidden", + }, +}); export interface OnSelectObject { path?: string; @@ -98,7 +102,7 @@ const H5PElement = ({ h5pUrl, onSelect, onClose, locale, canReturnResources }: P return ( - {fetchFailed && ( + {!!fetchFailed && ( )} - {url && } + {!!url && } ); }; diff --git a/src/components/H5PElement/h5pApi.ts b/src/components/H5PElement/h5pApi.ts index 0f9df6d0c3..66900fc40c 100644 --- a/src/components/H5PElement/h5pApi.ts +++ b/src/components/H5PElement/h5pApi.ts @@ -21,6 +21,10 @@ export interface H5PInfo { title: string; } +export interface H5pCopyResponse { + url: string; +} + export const fetchH5PiframeUrl = ( locale: string = "", canReturnResources: boolean = false, @@ -63,3 +67,15 @@ export const fetchH5PInfo = async (resourceId: string): Promise => { const url = `${config.h5pApiUrl}/v1/resource/${resourceId}/info`; return fetch(url).then((r) => resolveJsonOrRejectWithError(r)); }; + +export const copyH5P = async (url: string): Promise => { + const h5pUrl = `${config.h5pApiUrl}/copy`; + return await fetchReAuthorized(h5pUrl, { + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + Authorization: `Bearer JWT-token`, + }, + method: "POST", + body: `url=${encodeURIComponent(url)}`, + }).then((r) => resolveJsonOrRejectWithError(r)); +}; diff --git a/src/components/HeaderWithLanguage/DeleteLanguageVersion.tsx b/src/components/HeaderWithLanguage/DeleteLanguageVersion.tsx index b97d1bec05..09d5ae0ba2 100644 --- a/src/components/HeaderWithLanguage/DeleteLanguageVersion.tsx +++ b/src/components/HeaderWithLanguage/DeleteLanguageVersion.tsx @@ -9,7 +9,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { DeleteBinLine } from "@ndla/icons/action"; +import { DeleteBinLine } from "@ndla/icons"; import { Button } from "@ndla/primitives"; import { useMessages } from "../../containers/Messages/MessagesProvider"; import { deleteLanguageVersionAudio, deleteLanguageVersionSeries } from "../../modules/audio/audioApi"; @@ -105,6 +105,8 @@ const DeleteLanguageVersion = ({ id, language, supportedLanguages, type, disable await deleteLanguageVersionDraft(id, language); navigate(toEditFrontPageArticle(id, otherSupportedLanguage!)); break; + default: + createMessage({ message: t("embed.unsupported", { type }) }); } } catch (error) { createMessage(formatErrorMessage(error as NdlaErrorPayload)); @@ -135,7 +137,11 @@ const DeleteLanguageVersion = ({ id, language, supportedLanguages, type, disable - + diff --git a/src/components/HeaderWithLanguage/EmbedInformation/EmbedConnection.tsx b/src/components/HeaderWithLanguage/EmbedInformation/EmbedConnection.tsx index d9cac103d5..b4be747198 100644 --- a/src/components/HeaderWithLanguage/EmbedInformation/EmbedConnection.tsx +++ b/src/components/HeaderWithLanguage/EmbedInformation/EmbedConnection.tsx @@ -8,7 +8,7 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { SubjectMaterial } from "@ndla/icons/contentType"; +import { FileListLine } from "@ndla/icons"; import { DialogBody, DialogContent, @@ -66,10 +66,11 @@ const EmbedConnection = ({ id, type, articles, setArticles, concepts, setConcept postSearch(searchObjects(id, type)).then((result) => { if (shouldUpdateState) setArticles(result.results); }); - (type === "image" || type === "audio") && + if (type === "image" || type === "audio") { postSearchConcepts(searchObjects(id, type)).then((result) => { if (shouldUpdateState) setConcepts?.(result.results); }); + } } return () => { @@ -90,7 +91,7 @@ const EmbedConnection = ({ id, type, articles, setArticles, concepts, setConcept aria-label={t(`form.embedConnections.info.${type}`)} title={t(`form.embedConnections.info.${type}`)} > - +
diff --git a/src/components/HeaderWithLanguage/HeaderActions.tsx b/src/components/HeaderWithLanguage/HeaderActions.tsx index 4f5ab845b0..20c011279f 100644 --- a/src/components/HeaderWithLanguage/HeaderActions.tsx +++ b/src/components/HeaderWithLanguage/HeaderActions.tsx @@ -9,9 +9,7 @@ import { useFormikContext } from "formik"; import { memo, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { FileCompare } from "@ndla/icons/action"; -import { Launch } from "@ndla/icons/common"; -import { Eye } from "@ndla/icons/editor"; +import { ArrowRightShortLine, ShareBoxLine, EyeFill } from "@ndla/icons"; import { Button } from "@ndla/primitives"; import { SafeLinkButton } from "@ndla/safelink"; import { styled } from "@ndla/styled-system/jsx"; @@ -66,7 +64,7 @@ const PreviewLightBox = memo(({ type, currentLanguage, article, concept }: Previ language={currentLanguage} activateButton={ } /> @@ -80,7 +78,7 @@ const PreviewLightBox = memo(({ type, currentLanguage, article, concept }: Previ target="_blank" > {t("form.previewLanguageArticle.button")} - + ); } else return null; @@ -166,20 +164,20 @@ const HeaderActions = ({ supportedLanguages={supportedLanguages} isSubmitting={isSubmitting} /> - {isNewLanguage && ( + {!!isNewLanguage && ( {t(`languages.${language}`)} )} {translatableTypes.includes(type) && - language === "nb" && - showTranslate && - !supportedLanguages.includes("nn") && ( - <> - - - - )} + language === "nb" && + showTranslate && + !supportedLanguages.includes("nn") ? ( + <> + + + + ) : null} {!noStatus && ( <> @@ -187,7 +185,7 @@ const HeaderActions = ({ )} - {lastPublishedVersion && ( + {!!lastPublishedVersion && ( <> - {t("form.previewVersion")} + {t("form.previewVersion")} } /> diff --git a/src/components/HeaderWithLanguage/HeaderCurrentLanguagePill.tsx b/src/components/HeaderWithLanguage/HeaderCurrentLanguagePill.tsx index a015cd1883..f326c33349 100644 --- a/src/components/HeaderWithLanguage/HeaderCurrentLanguagePill.tsx +++ b/src/components/HeaderWithLanguage/HeaderCurrentLanguagePill.tsx @@ -7,7 +7,7 @@ */ import { ComponentPropsWithoutRef, ReactNode } from "react"; -import { CheckboxCircleFill } from "@ndla/icons/editor"; +import { CheckboxCircleFill } from "@ndla/icons"; import { Button } from "@ndla/primitives"; import { styled } from "@ndla/styled-system/jsx"; diff --git a/src/components/HeaderWithLanguage/HeaderFavoriteStatus.tsx b/src/components/HeaderWithLanguage/HeaderFavoriteStatus.tsx index d109535bd3..364a65e774 100644 --- a/src/components/HeaderWithLanguage/HeaderFavoriteStatus.tsx +++ b/src/components/HeaderWithLanguage/HeaderFavoriteStatus.tsx @@ -7,7 +7,7 @@ */ import { useTranslation } from "react-i18next"; -import { HeartFill } from "@ndla/icons/action"; +import { HeartFill } from "@ndla/icons"; import { Text } from "@ndla/primitives"; import { styled } from "@ndla/styled-system/jsx"; import { useResourceStats } from "../../modules/myndla/myndlaQueries"; @@ -30,7 +30,7 @@ const getResourceType = (type: string | undefined) => { case "standard": case "topic-article": case "frontpage-article": - return "article,multidiciplinary"; + return "article,multidiciplinary,topic"; default: return type; } diff --git a/src/components/HeaderWithLanguage/HeaderInformation.tsx b/src/components/HeaderWithLanguage/HeaderInformation.tsx index 6686d091ef..405ad4b2b2 100644 --- a/src/components/HeaderWithLanguage/HeaderInformation.tsx +++ b/src/components/HeaderWithLanguage/HeaderInformation.tsx @@ -10,12 +10,8 @@ import { useField } from "formik"; import { ReactNode, memo, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import styled from "@emotion/styled"; -import { colors, spacing } from "@ndla/core"; -import { List } from "@ndla/icons/action"; -import { Podcast } from "@ndla/icons/common"; -import { Camera, Concept, Taxonomy, SquareAudio, Globe } from "@ndla/icons/editor"; import { Button, Heading } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; import { ContentTypeBadge, constants } from "@ndla/ui"; import HeaderStatusInformation from "./HeaderStatusInformation"; import { useMessages } from "../../containers/Messages/MessagesProvider"; @@ -24,73 +20,76 @@ import * as draftApi from "../../modules/draft/draftApi"; import handleError from "../../util/handleError"; import { toEditArticle } from "../../util/routeHelpers"; import { Plain } from "../../util/slatePlainSerializer"; -import Spinner from "../Spinner"; +import { SegmentHeader } from "../Form/SegmentHeader"; -export const StyledSplitter = styled.div` - width: 1px; - background: ${colors.brand.lighter}; - height: ${spacing.normal}; - margin: 0 ${spacing.xsmall}; -`; +const StyledSegmentHeader = styled(SegmentHeader, { + base: { + paddingBlock: "3xsmall", + marginBlock: "xsmall", + }, +}); -const StyledHeader = styled.div` - display: flex; - justify-content: space-between; - padding: ${spacing.small} 0 ${spacing.xsmall}; - margin: ${spacing.normal} 0 ${spacing.small}; - border-bottom: 2px solid ${colors.brand.light}; -`; +export const StyledSplitter = styled("div", { + base: { + width: "1px", + background: "stroke.default", + height: "medium", + marginInline: "3xsmall", + }, +}); -const StyledTitleHeaderWrapper = styled.div` - padding-left: ${spacing.small}; - display: flex; - align-items: center; - gap: ${spacing.small}; -`; +const StyledTitleHeaderWrapper = styled("div", { + base: { + display: "flex", + alignItems: "center", + width: "100%", + gap: "3xsmall", + }, +}); const { contentTypes } = constants; const types: Record = { standard: { form: "learningResourceForm", - icon: , + icon: , }, "topic-article": { form: "topicArticleForm", - icon: , + icon: , }, subjectpage: { form: "subjectpageForm", - icon: , + icon: , }, "frontpage-article": { form: "frontpageArticleForm", - icon: , + icon: , }, - image: { form: "imageForm", icon: }, + image: { form: "imageForm", icon: }, audio: { form: "audioForm", - icon: , + icon: , }, podcast: { form: "podcastForm", - icon: , + icon: , }, "podcast-series": { form: "podcastSeriesForm", - icon: , + icon: , }, concept: { form: "conceptForm", - icon: , + icon: , }, gloss: { form: "glossform", - icon: , + icon: , }, programme: { form: "programmepageForm", - icon: , + icon: , }, }; @@ -139,7 +138,9 @@ const HeaderInformation = ({ (async () => { if (!responsibleId) return; const userData = await fetchAuth0Users(responsibleId); - userData.length && setResponsibleName(userData[0].name); + if (userData.length) { + setResponsibleName(userData[0].name); + } })(); }, [responsibleId]); @@ -165,18 +166,19 @@ const HeaderInformation = ({ }, [createMessage, formIsDirty, id, language, navigate]); return ( - + {types[type].icon} {type === "gloss" && title ? ( ) : ( - {`${t(`${types[type].form}.title`)}${title ? `: ${title}` : ""}`} + + {title ?? t("form.createNew", { type: t(`contentTypes.${type}`) })} + )} {(type === "standard" || type === "topic-article") && ( - )} @@ -194,7 +196,7 @@ const HeaderInformation = ({ responsibleName={responsibleName} hasRSS={hasRSS} /> - + ); }; diff --git a/src/components/HeaderWithLanguage/HeaderLanguagePicker.tsx b/src/components/HeaderWithLanguage/HeaderLanguagePicker.tsx index ac577bdae9..4eb3e8ac04 100644 --- a/src/components/HeaderWithLanguage/HeaderLanguagePicker.tsx +++ b/src/components/HeaderWithLanguage/HeaderLanguagePicker.tsx @@ -7,8 +7,8 @@ */ import { useTranslation } from "react-i18next"; -import { AddLine } from "@ndla/icons/action"; -import { Button, MenuContent, MenuItem, MenuPositioner, MenuRoot, MenuTrigger } from "@ndla/primitives"; +import { AddLine } from "@ndla/icons"; +import { Button, MenuContent, MenuItem, MenuRoot, MenuTrigger } from "@ndla/primitives"; import { SafeLink } from "@ndla/safelink"; const LanguagePicker = ({ id, emptyLanguages, editUrl }: Props) => { @@ -20,15 +20,13 @@ const LanguagePicker = ({ id, emptyLanguages, editUrl }: Props) => { {t("form.variant.create")} - - - {emptyLanguages.map((language) => ( - - {language.title} - - ))} - - + + {emptyLanguages.map((language) => ( + + {language.title} + + ))} + ); }; diff --git a/src/components/HeaderWithLanguage/HeaderStatusInformation.tsx b/src/components/HeaderWithLanguage/HeaderStatusInformation.tsx index 86722452eb..8816978757 100644 --- a/src/components/HeaderWithLanguage/HeaderStatusInformation.tsx +++ b/src/components/HeaderWithLanguage/HeaderStatusInformation.tsx @@ -8,11 +8,9 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import oldstyled from "@emotion/styled"; -import { colors, fonts, spacing } from "@ndla/core"; -import { ErrorWarningFill, RssFeed } from "@ndla/icons/common"; -import { CheckboxCircleFill } from "@ndla/icons/editor"; -import { SafeLink } from "@ndla/safelink"; +import { ErrorWarningFill, RssLine, CheckboxCircleFill } from "@ndla/icons"; +import { Text } from "@ndla/primitives"; +import { SafeLinkIconButton } from "@ndla/safelink"; import { styled } from "@ndla/styled-system/jsx"; import { IConceptSummary } from "@ndla/types-backend/concept-api"; import { ILearningPathV2 } from "@ndla/types-backend/learningpath-api"; @@ -24,18 +22,14 @@ import config from "../../config"; import formatDate from "../../util/formatDate"; import { StatusTimeFill } from "../StatusTimeFill"; -export const StyledSplitter = oldstyled.div` - width: 1px; - background: ${colors.brand.lighter}; - height: ${spacing.normal}; - margin: 0 ${spacing.xsmall}; -`; - -const StyledStatusWrapper = oldstyled.div` - display: flex; - align-items: center; - white-space: nowrap; -`; +const StyledStatusWrapper = styled("div", { + base: { + display: "flex", + alignItems: "center", + whiteSpace: "nowrap", + gap: "3xsmall", + }, +}); export const getWarnStatus = (date?: string): "warn" | "expired" | undefined => { if (!date) return undefined; @@ -51,7 +45,6 @@ export const getWarnStatus = (date?: string): "warn" | "expired" | undefined => }; interface Props { - compact?: boolean; noStatus?: boolean; statusText?: string; isNewLanguage?: boolean; @@ -68,47 +61,14 @@ interface Props { favoriteCount?: number; } -const StyledStatus = oldstyled.p` - ${fonts.sizes("16", "1.1")}; - font-weight: ${fonts.weight.semibold}; - margin: 0 ${spacing.small} 0; - color: ${colors.brand.primary}; - &[data-compact="true"] { - ${fonts.sizes("10", "1.1")}; - margin: 0 ${spacing.xsmall} 0; - } - span { - display: block; - } -`; - -const StyledSmallText = oldstyled.small` - color: ${colors.text.light}; - ${fonts.sizes("16", "1.1")}; - padding-right: ${spacing.xsmall}; - font-weight: ${fonts.weight.normal}; - color: ${colors.brand.primary}; - &[data-compact="true"] { - color: #000; - ${fonts.sizes("9", "1.1")}; - } -`; - -const StyledCheckIcon = oldstyled(CheckboxCircleFill)` - height: ${spacing.normal}; - width: ${spacing.normal}; - fill: ${colors.support.green}; -`; - -const StyledRssIcon = oldstyled(RssFeed)` - height: ${spacing.normal}; - width: ${spacing.normal}; - fill: ${colors.support.green}; -`; - -const StyledLink = oldstyled(SafeLink)` - box-shadow: inset 0 0; -`; +const StyledText = styled(Text, { + base: { + display: "flex", + "& > *": { + flex: "1", + }, + }, +}); const StyledErrorWarningFill = styled(ErrorWarningFill, { base: { @@ -122,7 +82,6 @@ const HeaderStatusInformation = ({ isNewLanguage, published, multipleTaxonomy, - compact, type, id, setHasConnections, @@ -167,8 +126,10 @@ const HeaderStatusInformation = ({ ) : (type === "concept" || type === "gloss") && !inSearch ? ( ) : null} - {published && ( - - - + + )} - {multipleTaxonomy && ( + {!!multipleTaxonomy && ( )} {!hideFavoritedIcon && } - - - {`${t("form.responsible.label")}:`} +
+ + {`${t("form.responsible.label")}: `} {responsibleName || t("form.responsible.noResponsible")} - + {noStatus ? ( t("form.status.new_language") ) : ( - - {t("form.workflow.statusLabel")}: + + {`${t("form.workflow.statusLabel")}: `} {isNewLanguage ? t("form.status.new_language") : statusText || t("form.status.new")} - + )} - +
); } else if (type === "image") { @@ -233,11 +194,16 @@ const HeaderStatusInformation = ({ ); } else if (type === "podcast-series" && hasRSS && id !== undefined) { return ( - - - - - + + + ); } return null; diff --git a/src/components/HeaderWithLanguage/HeaderWithLanguage.tsx b/src/components/HeaderWithLanguage/HeaderWithLanguage.tsx index 9112947e64..594eb0b7fe 100644 --- a/src/components/HeaderWithLanguage/HeaderWithLanguage.tsx +++ b/src/components/HeaderWithLanguage/HeaderWithLanguage.tsx @@ -8,8 +8,6 @@ import { memo, useState } from "react"; import { useTranslation } from "react-i18next"; -import styled from "@emotion/styled"; -import { spacing } from "@ndla/core"; import { IConcept } from "@ndla/types-backend/concept-api"; import { IArticle, IStatus } from "@ndla/types-backend/draft-api"; import { TaxonomyContext } from "@ndla/types-taxonomy"; @@ -17,13 +15,6 @@ import HeaderActions from "./HeaderActions"; import { HeaderCurrentLanguagePill } from "./HeaderCurrentLanguagePill"; import HeaderInformation from "./HeaderInformation"; -export const StyledLanguageWrapper = styled.div` - padding-left: ${spacing.small}; - margin: 0; - display: flex; - align-items: center; -`; - export type FormHeaderType = | "image" | "audio" @@ -95,24 +86,22 @@ const HeaderWithLanguage = ({ language={language} slug={article?.slug} /> - - {id ? ( - - ) : ( - {t(`languages.${language}`)} - )} - + {id ? ( + + ) : ( + {t(`languages.${language}`)} + )} ); }; diff --git a/src/components/HeaderWithLanguage/LearningpathConnection.tsx b/src/components/HeaderWithLanguage/LearningpathConnection.tsx index 894e20b5e8..ddcc767f0d 100644 --- a/src/components/HeaderWithLanguage/LearningpathConnection.tsx +++ b/src/components/HeaderWithLanguage/LearningpathConnection.tsx @@ -8,7 +8,7 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { LearningPath } from "@ndla/icons/contentType"; +import { TextWrap } from "@ndla/icons"; import { DialogBody, DialogContent, @@ -52,7 +52,7 @@ const LearningpathConnection = ({ id, learningpaths, setLearningpaths }: Props) aria-label={t("form.learningpathConnections.sectionTitle")} title={t("form.learningpathConnections.sectionTitle")} > - + diff --git a/src/components/HeaderWithLanguage/SimpleLanguageHeader.tsx b/src/components/HeaderWithLanguage/SimpleLanguageHeader.tsx index 9140c96b38..9203d9c35f 100644 --- a/src/components/HeaderWithLanguage/SimpleLanguageHeader.tsx +++ b/src/components/HeaderWithLanguage/SimpleLanguageHeader.tsx @@ -7,11 +7,18 @@ */ import { useTranslation } from "react-i18next"; +import { styled } from "@ndla/styled-system/jsx"; import { HeaderCurrentLanguagePill } from "./HeaderCurrentLanguagePill"; import HeaderInformation, { StyledSplitter } from "./HeaderInformation"; import HeaderLanguagePicker from "./HeaderLanguagePicker"; import HeaderSupportedLanguages from "./HeaderSupportedLanguages"; -import { StyledLanguageWrapper } from "./HeaderWithLanguage"; + +const Wrapper = styled("div", { + base: { + display: "flex", + alignItems: "center", + }, +}); interface Props { articleType: string; @@ -51,7 +58,7 @@ const SimpleLanguageHeader = ({ ); return ( - <> +
- - {id ? ( - <> - - {isNewLanguage && ( - - {t(`languages.${language}`)} - - )} - - - - ) : ( - <> - {t(`languages.${language}`)} - - )} - - + {id ? ( + + + {!!isNewLanguage && ( + + {t(`languages.${language}`)} + + )} + + + + ) : ( + {t(`languages.${language}`)} + )} +
); }; diff --git a/src/components/HowTo/HowToHelper.tsx b/src/components/HowTo/HowToHelper.tsx index c4460eb40d..d3e8ecba28 100644 --- a/src/components/HowTo/HowToHelper.tsx +++ b/src/components/HowTo/HowToHelper.tsx @@ -8,7 +8,7 @@ import { memo } from "react"; import { Portal } from "@ark-ui/react"; -import { InformationOutline } from "@ndla/icons/common"; +import { InformationLine } from "@ndla/icons"; import { Text, DialogBody, @@ -37,7 +37,7 @@ const HowToHelper = ({ pageId, tooltip }: Props) => { - + @@ -45,7 +45,7 @@ const HowToHelper = ({ pageId, tooltip }: Props) => { - + {story.title} diff --git a/src/components/LastUpdatedLine/DateEdit.tsx b/src/components/LastUpdatedLine/DateEdit.tsx index c5b63f8a6f..f90ed9f5d0 100644 --- a/src/components/LastUpdatedLine/DateEdit.tsx +++ b/src/components/LastUpdatedLine/DateEdit.tsx @@ -6,7 +6,7 @@ * */ -import { PencilFill } from "@ndla/icons/action"; +import { PencilFill } from "@ndla/icons"; import { Button } from "@ndla/primitives"; import formatDate, { formatDateForBackend } from "../../util/formatDate"; import DatePicker from "../DatePicker"; diff --git a/src/components/LastUpdatedLine/LastUpdatedLine.tsx b/src/components/LastUpdatedLine/LastUpdatedLine.tsx index d6a866304c..6c25b35c78 100644 --- a/src/components/LastUpdatedLine/LastUpdatedLine.tsx +++ b/src/components/LastUpdatedLine/LastUpdatedLine.tsx @@ -34,7 +34,7 @@ const LastUpdatedLine = ({ creators, published, onChange, allowEdit = false, con {creators.map((creator) => creator.name).join(", ")} {published ? ` - ${dateLabel}: ` : ""} - {published && (allowEdit ? : formatDate(published))} + {!!published && (allowEdit ? : formatDate(published))} ); }; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx deleted file mode 100644 index a1386bd4a5..0000000000 --- a/src/components/Layout/Layout.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2023-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import styled from "@emotion/styled"; -import { spacing, mq } from "@ndla/core"; - -export const GRID_GAP = spacing.nsmall; - -export const GridContainer = styled.div<{ breakpoint?: string }>` - ${({ breakpoint }) => breakpoint && mq.range({ from: "0px", until: breakpoint })} { - padding: 0 ${GRID_GAP}; - display: flex; - flex-direction: column; - gap: ${GRID_GAP}; - } - - ${({ breakpoint }) => mq.range({ from: breakpoint ?? "0px" })} { - display: grid; - grid-template-columns: repeat(12, 1fr); - grid-gap: ${GRID_GAP}; - max-width: 1400px; - justify-self: center; - align-self: center; - width: 100%; - padding: 0 ${spacing.nsmall}; - } -`; -interface ColumnSize { - colStart?: number; - colEnd?: number; -} - -export const Column = styled.div` - grid-column: ${(props) => `${props.colStart ?? 1} / ${props.colEnd ?? 13}`}; - min-width: 400px; -`; diff --git a/src/components/Layout/MastheadLayout.tsx b/src/components/Layout/MastheadLayout.tsx index 62511d87e4..fa0d0b0a2f 100644 --- a/src/components/Layout/MastheadLayout.tsx +++ b/src/components/Layout/MastheadLayout.tsx @@ -6,12 +6,20 @@ * */ +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; import { Outlet } from "react-router-dom"; import { Masthead } from "../../containers/Masthead/Masthead"; +import Messages from "../../containers/Messages/Messages"; -export const MastheadLayout = () => ( - <> - - - -); +export const MastheadLayout = () => { + const { t } = useTranslation(); + return ( + <> + + + + + + ); +}; diff --git a/src/components/Layout/PageLayout.tsx b/src/components/Layout/PageLayout.tsx new file mode 100644 index 0000000000..aad5c57cd3 --- /dev/null +++ b/src/components/Layout/PageLayout.tsx @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { ComponentPropsWithRef, CSSProperties, forwardRef, useMemo } from "react"; +import { useComponentSize } from "@ndla/hooks"; +import { styled } from "@ndla/styled-system/jsx"; +import { JsxStyleProps } from "@ndla/styled-system/types"; + +const StyledPageLayout = styled("div", { + base: { + display: "flex", + flexDirection: "column", + // The minimum page height should be 100vh - masthead height + minHeight: "calc(100vh - (var(--masthead-height, 72px)))", + }, +}); + +export const PageLayout = forwardRef & JsxStyleProps>((props, ref) => { + const { height } = useComponentSize("masthead"); + const mastheadHeightVar = useMemo(() => ({ "--masthead-height": `${height}px` }) as CSSProperties, [height]); + return ; +}); diff --git a/src/components/MetaInformation.tsx b/src/components/MetaInformation.tsx index 932aecba88..8f66520e4d 100644 --- a/src/components/MetaInformation.tsx +++ b/src/components/MetaInformation.tsx @@ -37,7 +37,7 @@ const MetaInformation = ({ title, copyright, action, alt }: Props) => { const { t } = useTranslation(); return ( - {action &&
{action}
} + {!!action &&
{action}
} {!!title && (
diff --git a/src/components/MonacoEditor/MonacoEditor.tsx b/src/components/MonacoEditor/MonacoEditor.tsx index fee60321bf..878887d8d1 100644 --- a/src/components/MonacoEditor/MonacoEditor.tsx +++ b/src/components/MonacoEditor/MonacoEditor.tsx @@ -31,7 +31,6 @@ import { createFormatAction, createSaveAction } from "./editorActions"; const StyledDiv = styled("div", { base: { - margin: "medium", border: "1px solid", borderColor: "stroke.subtle", }, @@ -66,7 +65,6 @@ monaco.editor.defineTheme("myCustomTheme", { colors: {}, }); -// eslint-disable-next-line no-restricted-globals self.MonacoEnvironment = { getWorker() { return new htmlWorker(); diff --git a/src/components/MoveContentButton.tsx b/src/components/MoveContentButton.tsx index 77300bdbe3..46d3c2313c 100644 --- a/src/components/MoveContentButton.tsx +++ b/src/components/MoveContentButton.tsx @@ -6,7 +6,7 @@ * */ -import { ArrowLeftShortLine } from "@ndla/icons/common"; +import { ArrowLeftShortLine } from "@ndla/icons"; import { IconButton, IconButtonProps } from "@ndla/primitives"; export const MoveContentButton = ({ onMouseDown, ...rest }: IconButtonProps) => { diff --git a/src/components/MultiButton.tsx b/src/components/MultiButton.tsx deleted file mode 100644 index 637dca2ed5..0000000000 --- a/src/components/MultiButton.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2022-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { ReactElement } from "react"; -import styled from "@emotion/styled"; -import { Content, Item, Portal, Root, Trigger } from "@radix-ui/react-dropdown-menu"; -import { ButtonV2 } from "@ndla/button"; -import { colors, shadows, stackOrder } from "@ndla/core"; -import { ArrowDownShortLine } from "@ndla/icons/common"; - -const MainButton = styled(ButtonV2)` - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: none; -`; - -const Wrapper = styled.div` - display: flex; -`; - -const Spacer = styled.div` - background-color: white; - width: 1px; - border-top: 2px solid ${colors.brand.primary}; - border-bottom: 2px solid ${colors.brand.primary}; - &[data-disabled] { - border-bottom-color: ${colors.background.dark}; - border-top-color: ${colors.background.dark}; - } -`; - -interface ButtonProps { - label: string; - value: string; - enable?: boolean; -} - -interface Props { - mainButton: ButtonProps; - secondaryButtons: Array; - onClick: (value: string) => void; - disabled?: boolean; - large?: boolean; - mainId?: string; - menuPosition?: "top" | "bottom"; - children?: ReactElement; -} - -const MenuButton = styled(ButtonV2)` - display: flex; - width: 100%; - border: 0; - &:not(:first-child):not(:last-child) { - border-radius: 0; - border-top: 1px solid ${colors.white}; - } - &:first-of-type { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - &:last-of-type { - border-top: 1px solid ${colors.white}; - border-top-left-radius: 0; - border-top-right-radius: 0; - } -`; - -const MenuItems = styled(Content)` - box-shadow: ${shadows.levitate1}; - z-index: ${stackOrder.popover}; -`; - -const ToggleButton = styled(ButtonV2)` - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left: none; - transition: all 200ms ease; - &[data-state="open"] { - svg { - transform: rotate(180deg); - } - } -`; - -export const MultiButton = ({ - mainButton, - secondaryButtons, - onClick, - disabled, - large, - menuPosition = "top", - mainId, - children, -}: Props) => { - const hideSecondaryButton = secondaryButtons.length === 0; - - const isDisabled = secondaryButtons.find((button) => button.enable) ? false : disabled; - - return ( - - onClick(mainButton.value)} - > - {children || mainButton.label} - - {!hideSecondaryButton && ( - <> - - - - - - - - - - {secondaryButtons.map((button) => ( - onClick(button.value)} - > - - {button.label} - - - ))} - - - - - )} - - ); -}; - -export default MultiButton; diff --git a/src/components/NodeIconType.tsx b/src/components/NodeIconType.tsx index 745624eb27..7de45071a0 100644 --- a/src/components/NodeIconType.tsx +++ b/src/components/NodeIconType.tsx @@ -6,23 +6,12 @@ * */ import { useTranslation } from "react-i18next"; -import styled from "@emotion/styled"; -import { colors } from "@ndla/core"; -import { MenuBook } from "@ndla/icons/action"; -import { Subject } from "@ndla/icons/contentType"; +import { BookOpenLine, FileListLine } from "@ndla/icons"; import { Node } from "@ndla/types-taxonomy"; import { DiffType } from "../containers/NodeDiff/diffUtils"; import { SUBJECT_NODE } from "../modules/nodes/nodeApiTypes"; import { getNodeTypeFromNodeId } from "../modules/nodes/nodeUtil"; -const StyledMenuBook = styled(MenuBook)` - height: 31px; - width: 31px; - color: ${colors.brand.primary}; -`; - -const StyledSubject = StyledMenuBook.withComponent(Subject); - interface Props { node: DiffType | Node; } @@ -34,7 +23,7 @@ const NodeIconType = ({ node }: Props) => { ? getNodeTypeFromNodeId(node.id) : getNodeTypeFromNodeId(node.id.other ?? node.id.original!); - const Icon = nodeType === SUBJECT_NODE ? StyledMenuBook : StyledSubject; + const Icon = nodeType === SUBJECT_NODE ? BookOpenLine : FileListLine; return ; }; diff --git a/src/components/ObjectSelector.tsx b/src/components/ObjectSelector.tsx index 2f7d399c7c..59158321dc 100644 --- a/src/components/ObjectSelector.tsx +++ b/src/components/ObjectSelector.tsx @@ -6,71 +6,74 @@ * */ -import { FormEvent, MouseEvent } from "react"; -import { uuid } from "@ndla/util"; +import { useMemo } from "react"; +import { createListCollection } from "@ark-ui/react"; +import { SelectRoot, SelectLabel, SelectValueText, SelectContent } from "@ndla/primitives"; +import { styled } from "@ndla/styled-system/jsx"; +import { GenericSelectItem, GenericSelectTrigger } from "./abstractions/Select"; + +const StyledGenericSelectTrigger = styled(GenericSelectTrigger, { + base: { + width: "100%", + }, +}); + +const StyledSelectValueText = styled(SelectValueText, { + base: { + lineClamp: "1", + overflowWrap: "anywhere", + }, +}); + +const StyledGenericSelectItem = styled(GenericSelectItem, { + base: { + overflowWrap: "anywhere", + }, +}); + +export interface SelectOption { + id: string; + name: string; +} interface Props { - options?: Record[]; + options: SelectOption[]; value: string; - optGroups?: { label: string; options: Record[] }[]; - onChange: (event: FormEvent) => void; - onBlur?: (event: FormEvent) => void; - labelKey: string; - idKey: string; - disabled?: boolean; - className?: string; - emptyField?: boolean; - placeholder?: string; + onChange: (value: string) => void; + placeholder: string; name: string; - onClick?: (event: MouseEvent) => void; } -const ObjectSelector = ({ - options, - labelKey, - idKey, - disabled = false, - onChange, - onBlur, - value, - emptyField = false, - placeholder = "", - className = "", - name, - onClick, - optGroups, -}: Props) => { +const ObjectSelector = ({ options, onChange, value, placeholder, name }: Props) => { + const collection = useMemo( + () => + createListCollection({ + items: options, + itemToValue: (item) => item.id, + itemToString: (item) => item.name, + }), + [options], + ); + return ( - + {placeholder} + + + + + {collection.items.map((option) => ( + + {option.name} + + ))} + + ); }; diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx deleted file mode 100644 index 31406bdd8a..0000000000 --- a/src/components/Overlay.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2017-present, NDLA. - * - * This source code is licensed under the GPLv3 license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { MouseEvent } from "react"; -import { css, SerializedStyles } from "@emotion/react"; -import styled from "@emotion/styled"; -import { animations, stackOrder } from "@ndla/core"; - -const appearances: Record = { - zIndex: css` - z-index: ${stackOrder.dropdown - stackOrder.offsetSingle}; - `, - absolute: css` - position: absolute; - width: 100%; - height: 100%; - `, - "white-opacity": css` - opacity: 0.8; - background-color: white; - `, - lighter: css` - background: rgba(1, 1, 1, 0.3); - z-index: ${stackOrder.offsetDouble}; - `, -}; - -const getAllAppearances = (modifiers: string | string[]) => { - if (Array.isArray(modifiers)) { - return modifiers.map((modifier) => appearances[modifier]); - } - return appearances[modifiers]; -}; - -const StyledOverlay = styled.div<{ modifiers: string | string[] }>` - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 100vw; - z-index: ${stackOrder.offsetDouble}; - background: rgba(0, 0, 0, 0.3); - - ${animations.fadeIn()} - ${(p) => getAllAppearances(p.modifiers)} -`; - -interface Props { - onExit?: (event: MouseEvent) => void; - modifiers?: string | string[]; -} - -const Overlay = ({ onExit, modifiers = "" }: Props) => - onExit ? ( -