Compare commits

...
Author SHA1 Message Date
Ken 3a1fc4b180 ✨ Nyere alpine build 2022-10-29 19:15:54 +02:00
Ken c8efc8da90 🐛 Fikset docker build 2022-10-29 19:06:26 +02:00
Ken b81e1ce3b7 🐛 Docker-build fra riktig context 2022-10-29 17:02:35 +02:00
Ken fa9e6f0e48 🚨 fikset flaky sidebar-test 2022-10-29 16:43:28 +02:00
Ken 7c93704cc2 🐛 docker-build 2022-10-29 16:40:25 +02:00
Ken be27485a41 🎨 chromatic-tester bare ve push til master 2022-10-29 16:10:31 +02:00
Ken cdd1a6e6b5 🐛 typer nå med i git 2022-10-29 16:09:14 +02:00
Ken 6824abff7e 🎨 svgr --silent flag 2022-10-29 15:54:21 +02:00
Ken 5b3e1d192e 🎨 la til sanity-namespace 2022-10-29 15:52:23 +02:00
Ken aea6305d29 🔥 Fjernet ubrukt secret 2022-10-29 15:30:56 +02:00
Ken 924dc335c2 🚚 whitelist gitignore-filer 2022-10-29 15:12:27 +02:00
Ken cdd3237854 🚚 Bedre sidebar-test 2022-10-29 15:04:52 +02:00
Ken 15893e2e7d 🚚 rename e2e 2022-10-29 13:32:03 +02:00
Ken 3fcbbdabb8 🎨 riktig include i package.json 2022-10-29 13:23:59 +02:00
Ken db78728006 ✨ incremental build 2022-10-29 13:19:19 +02:00
Ken 5de37d88cc 🚚 postintall 2022-10-29 12:25:52 +02:00
Ken d5f61c5dcb 💚 bygger ds-pakker før aksel 2022-10-29 02:40:08 +02:00
Ken 6d4eb0b6e7 🔥 Fjernet cypress 2022-10-29 02:35:37 +02:00
Ken 1848fc1475 👷 playwright postinstall 2022-10-29 02:32:22 +02:00
Ken 791dd28c31 👷 e2e workflow 2022-10-29 02:27:27 +02:00
Ken 59caa4322f 👷 rename smoketst 2022-10-29 02:16:31 +02:00
Ken 5667cad2b4 👷 playwright-tester 2022-10-29 01:48:36 +02:00
Ken 4facae1d73 👷 playwright 2022-10-29 01:26:28 +02:00
Ken 662a8cbe2f 🧪 2022-10-28 23:58:31 +02:00
Ken be652d3497 👷 fungerende tester 2022-10-28 23:44:16 +02:00
Ken 8a10b3320f 💚 env 2022-10-28 22:23:19 +02:00
Ken 6ce7aceef0 💚 riktig ref 2022-10-28 21:43:04 +02:00
Ken 79ccc8cb53 🎨 riktig branch for prod-build 2022-10-28 21:33:52 +02:00
Ken 4134b89519 📝 docs 2022-10-28 21:29:37 +02:00
Ken 9e5045a8ad 👷 2022-10-28 21:21:28 +02:00
Ken 01dc0edd8c 🎨 fikset ikonsøk 2022-10-28 21:12:47 +02:00
Ken bc59f7c9be 🎨 cypress 2022-10-28 21:08:33 +02:00
Ken 5c67229808 🎨 gitignore 2022-10-28 21:06:22 +02:00
Ken 705f5f465c 🚨 nextjs 2022-10-28 21:01:50 +02:00
Ken 23e9ff0a2b 🚨 nextjs 2022-10-28 20:41:00 +02:00
Ken fa69860bb4 🚨 2022-10-28 20:14:24 +02:00
Ken 49dc2acb12 ⬆️ react-dom sync 2022-10-28 20:06:03 +02:00
Ken e537b9ac07 🔥 Fjernet ubrukte deps 2022-10-28 19:48:00 +02:00
Ken 8fc0bbc5b2 💚 riktige scripts 2022-10-28 19:40:54 +02:00
Ken d3701896f7 🔥 Fjernet cypress-tester 2022-10-28 19:34:52 +02:00
Ken a671c8d4ba 🚚 Aksel.nav.no 2022-10-28 19:32:31 +02:00
Ken c20af56394 🚚 Aksel.nav.no 2022-10-28 19:30:08 +02:00
494 changed files with 41173 additions and 493 deletions

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": ["nextjs-demo", "shadow-dom"],
"ignore": ["nextjs-demo", "shadow-dom", "aksel.nav.no"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"updateInternalDependents": "always"
+78
View File
@@ -0,0 +1,78 @@
name: Deploy aksel.dev.nav.no
on:
workflow_dispatch:
push:
branches:
- next
env:
IMAGE: ghcr.io/navikt/designsystemet
jobs:
build_and_deploy:
name: Build and deploy to dev-gcp
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-node@v1
with:
node-version: "14"
- uses: actions/checkout@v2
with:
ref: "next"
- name: add tokens to .env
run: |
echo "SANITY_WRITE_KEY=${{ secrets.SANITY_FEEDBACK_WRITE_KEY }}" >> aksel.nav.no/website/.env
echo "ALGOLIA_ADMIN=${{ secrets.ALGOLIA_ADMIN }}" >> aksel.nav.no/website/.env
echo "ALGOLIA_ID=${{ secrets.ALGOLIA_ID}}" >> aksel.nav.no/website/.env
echo "TRELLO_TOKEN=${{ secrets.TRELLO_TOKEN }}" >> aksel.nav.no/website/.env
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PREVIEW_TOKEN=${{ secrets.SANITY_PREVIEW_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PRIVATE_NO_DRAFTS=${{ secrets.SANITY_PRIVATE_NO_DRAFTS }}" >> aksel.nav.no/website/.env
- name: Restore cache
uses: actions/cache@v3
with:
path: "**/node_modules"
key: ${{ runner.os }}-dev-${{ github.sha }}
restore-keys: |
${{ runner.os }}-dev-
- name: Install dependencies
run: yarn
- name: Build application
run: |
yarn boot
yarn docgen
yarn workspace aksel.nav.no build:next
- name: Login to GitHub Container Registry
uses: docker/login-action@ab80d026d4753220c4243394c07c7d80f9638d06 # Use commit-sha1 instead of tag for security concerns
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get complete tag
run: echo "TAG=DEV-$( date +%s )" >> $GITHUB_ENV
- name: Create docker image tags
run: echo "DOCKER_TAG=${{ env.IMAGE }}:${{ env.TAG }}" >> $GITHUB_ENV
- name: Build docker image
run: docker build -t ${{ env.DOCKER_TAG }} . -f aksel.nav.no/Dockerfile
- name: Push docker image
run: |
docker push ${{ env.DOCKER_TAG }}
- name: Create tag and release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.TAG }}
release_name: ${{ env.TAG }}
commitish: ${{ github.sha }}
- name: Deploy to dev-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: aksel.nav.no/nais-dev.yaml
CLUSTER: dev-gcp
IMAGE: ${{ env.DOCKER_TAG }}
+88
View File
@@ -0,0 +1,88 @@
name: Deploy aksel.nav.no
on:
push:
branches:
- master
paths:
- "aksel.nav.no/**"
env:
IMAGE: ghcr.io/navikt/designsystemet
jobs:
build_and_deploy:
name: Build and deploy to prod-gcp
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-node@v1
with:
node-version: "14"
- uses: actions/checkout@v2
- name: add tokens to .env
run: |
echo "SANITY_WRITE_KEY=${{ secrets.SANITY_FEEDBACK_WRITE_KEY }}" >> aksel.nav.no/website/.env
echo "ALGOLIA_ADMIN=${{ secrets.ALGOLIA_ADMIN }}" >> aksel.nav.no/website/.env
echo "ALGOLIA_ID=${{ secrets.ALGOLIA_ID}}" >> aksel.nav.no/website/.env
echo "TRELLO_TOKEN=${{ secrets.TRELLO_TOKEN }}" >> aksel.nav.no/website/.env
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PREVIEW_TOKEN=${{ secrets.SANITY_PREVIEW_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PRIVATE_NO_DRAFTS=${{ secrets.SANITY_PRIVATE_NO_DRAFTS }}" >> aksel.nav.no/website/.env
- name: Restore cache
uses: actions/cache@v3
with:
path: "**/node_modules"
key: ${{ runner.os }}-prod-${{ github.sha }}
restore-keys: |
${{ runner.os }}-prod-
- name: Install dependencies
run: yarn
- name: build packages
run: |
yarn boot
yarn docgen
- name: Tests
run: yarn workspace aksel.nav.no test
- name: E2E
run: NEXT_PUBLIC_TEST=true yarn workspace website e2e
- name: Update sanity sync
run: yarn workspace aksel.nav.no sanity:update
- name: Build application
run: yarn workspace aksel.nav.no build:next
- name: Login to GitHub Container Registry
uses: docker/login-action@ab80d026d4753220c4243394c07c7d80f9638d06 # Use commit-sha1 instead of tag for security concerns
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get complete tag
run: echo "TAG=$( date +%s )" >> $GITHUB_ENV
- name: Create docker image tags
run: echo "DOCKER_TAG=${{ env.IMAGE }}:${{ env.TAG }}" >> $GITHUB_ENV
- name: Build docker image
run: docker build -t ${{ env.DOCKER_TAG }} . -f aksel.nav.no/Dockerfile
- name: Push docker image
run: |
docker push ${{ env.DOCKER_TAG }}
- name: Create tag and release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.TAG }}
release_name: ${{ env.TAG }}
commitish: ${{ github.sha }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ env.DOCKER_TAG }}
+41
View File
@@ -0,0 +1,41 @@
name: Redeploy image to for aksel.nav.no
on:
workflow_dispatch:
inputs:
tag:
description: "Docker image tag"
required: true
default: ""
dry:
description: "Dry-run"
required: true
default: "false"
env:
IMAGE: ghcr.io/navikt/designsystemet
jobs:
redeploy:
name: Redeploy image
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-node@v1
with:
node-version: "14"
- uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@ab80d026d4753220c4243394c07c7d80f9638d06 # Use commit-sha1 instead of tag for security concerns
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create docker image tag
run: echo "DOCKER_TAG=${{ env.IMAGE }}:${{ github.event.inputs.tag }}" >> $GITHUB_ENV
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ env.DOCKER_TAG }}
DRY_RUN: ${{ github.event.inputs.dry }}
+39
View File
@@ -0,0 +1,39 @@
name: Backup sanity dateset to gcp
on:
workflow_dispatch:
schedule:
# 12.00 every 3 days
- cron: "0 0 */3 * *"
jobs:
execute:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Required to retrieve git history
- uses: actions/setup-node@v1
with:
node-version: 14
- name: Restore cache
uses: actions/cache@v3
with:
path: "**/node_modules"
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Install dependencies
run: yarn
- name: Export dataset
run: SANITY_TOKEN=${{secrets.SANITY_DATASET_EDITOR_TOKEN}} yarn workspace aksel.nav.no sanity:backup
- name: Upload GCP bucket
uses: "google-github-actions/auth@v0"
with:
credentials_json: "${{ secrets.gcp_credentials_prod }}"
- uses: "google-github-actions/upload-cloud-storage@v0"
with:
path: ./aksel.nav.no/sanity
destination: "aksel-website-prod/backups"
glob: "**/backup-*.tar.gz"
+1 -2
View File
@@ -4,11 +4,10 @@ on:
push:
branches:
- "master"
- "next"
paths:
- "@navikt/**"
pull_request:
branches: [master]
branches: [master, next]
paths:
- "@navikt/**"
+71
View File
@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [master, next]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: "41 8 * * 6"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+56
View File
@@ -0,0 +1,56 @@
name: E2E / Playwright Tests
on:
push:
branches: [master, next]
paths:
- "aksel.nav.no/**"
pull_request:
branches: [master, next]
paths:
- "aksel.nav.no/**"
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: add tokens to .env
run: |
echo "SANITY_WRITE_KEY=${{ secrets.SANITY_FEEDBACK_WRITE_KEY }}" >> aksel.nav.no/website/.env
echo "ALGOLIA_ADMIN=${{ secrets.ALGOLIA_ADMIN }}" >> aksel.nav.no/website/.env
echo "ALGOLIA_ID=${{ secrets.ALGOLIA_ID}}" >> aksel.nav.no/website/.env
echo "TRELLO_TOKEN=${{ secrets.TRELLO_TOKEN }}" >> aksel.nav.no/website/.env
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PREVIEW_TOKEN=${{ secrets.SANITY_PREVIEW_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PRIVATE_NO_DRAFTS=${{ secrets.SANITY_PRIVATE_NO_DRAFTS }}" >> aksel.nav.no/website/.env
- name: Restore cache
uses: actions/cache@v3
with:
path: "**/node_modules"
key: ${{ runner.os }}-e2e-${{ github.sha }}
restore-keys: |
${{ runner.os }}-e2e-
- name: Install dependencies
run: yarn
- name: build packages
run: |
yarn boot
yarn docgen
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: yarn workspace website e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: aksel.nav.no/website/playwright-report/
retention-days: 10
+2 -2
View File
@@ -27,9 +27,9 @@ jobs:
uses: actions/cache@v3
with:
path: "**/node_modules"
key: ${{ runner.os }}-test-${{ github.sha }}
key: ${{ runner.os }}-prod-${{ github.sha }}
restore-keys: |
${{ runner.os }}-test
${{ runner.os }}-prod
- name: Install dependencies
run: yarn
+25
View File
@@ -0,0 +1,25 @@
name: Indexing search
on:
workflow_dispatch:
schedule:
# 01.00 & 12.00 every day
- cron: "00 01,12 * * *"
jobs:
searchcrawl:
name: Search-indexing aksel.nav.no
runs-on: ubuntu-latest
env:
APPLICATION_ID: ${{ secrets.ALGOLIA_ID}}
API_KEY: ${{ secrets.ALGOLIA_ADMIN }}
steps:
- uses: actions/checkout@v2
- name: Read crawler.json
id: algolia_config
run: echo "::set-output name=config::$(cat ./aksel.nav.no/crawler.json | jq -r tostring)"
- name: Push indices to Algolia
uses: signcl/docsearch-scraper-action@master
env:
APPLICATION_ID: ${{ secrets.ALGOLIA_ID}}
API_KEY: ${{ secrets.ALGOLIA_ADMIN }}
CONFIG: ${{ steps.algolia_config.outputs.config }}
+19 -1
View File
@@ -129,4 +129,22 @@ _docs.json
*/icons/src
*/icons/png
*/core/icons
/storybook-static
/storybook-static
# Aksel.nav.no
.next
!*/website/lib/
!*/website/public/
!aksel.nav.no/**/*.d.ts
!*/sanity/lib/
!*/sanity/static/
out
test-urls.json
videos
sanity/scripts/renameField.js
tmp.json
/test-results/
/playwright-report/
/playwright/.cache/
playwright-report/
/test-results/
+2
View File
@@ -0,0 +1,2 @@
**/nais-dev.yaml
**/nais-prod.yaml
+1
View File
@@ -44,6 +44,7 @@
"prettier": "2.7.1"
},
"devDependencies": {
"@types/jest": "^27.5.0",
"@types/jscodeshift": "0.11.5",
"@types/react": "^18.0.0",
"del": "7.0.0",
+7 -5
View File
@@ -24,13 +24,14 @@
"/cjs",
"/esm",
"/src",
"_docs.json"
"_docs.json",
"!**/*.tsbuildinfo"
],
"scripts": {
"docgen": "yarn ts-node ../../../scripts/docgen.ts",
"create-package-json-pointers-to-esm": "node ../../../scripts/createPackageJsonsWithESMPointers.js",
"clean": "rimraf cjs esm",
"build": "yarn run clean && concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && yarn create-package-json-pointers-to-esm",
"build": "concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && yarn create-package-json-pointers-to-esm",
"watch": "tsc --watch -p tsconfig.esm.json",
"test": "jest"
},
@@ -50,19 +51,20 @@
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0",
"@types/faker": "5.5.8",
"@types/jest": "^27.0.1",
"@types/jest": "^29.0.0",
"@types/node": "^17.0.35",
"@types/react-modal": "^3.13.1",
"concurrently": "7.2.1",
"copyfiles": "^2.4.1",
"faker": "5.5.3",
"fast-glob": "3.2.11",
"jest": "^27.5.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^6.3.0",
"rimraf": "3.0.2",
"swr": "^1.1.2",
"ts-jest": "^27.1.0",
"ts-jest": "^29.0.0",
"ts-node": "10.7.0",
"typescript": "^4.7.2"
},
+4 -3
View File
@@ -25,13 +25,14 @@
"/esm",
"/src",
"/svg",
"/meta.json"
"/meta.json",
"!**/*.tsbuildinfo"
],
"scripts": {
"create-package-json-pointers-to-esm": "node ../../scripts/createPackageJsonsWithESMPointers.js",
"create-icons": "svgr --out-dir src svg",
"create-icons": "svgr --silent --out-dir src svg",
"clean": "rimraf src cjs esm",
"build": "yarn clean && copyfiles util/* src && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && yarn create-package-json-pointers-to-esm",
"build": "copyfiles util/* src && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && yarn create-package-json-pointers-to-esm",
"update": "ts-node figma/index.ts dotenv/config",
"test": "jest"
},
+2 -1
View File
@@ -4,5 +4,6 @@
"outDir": "./cjs",
"module": "commonjs"
},
"exclude": ["./figma", "./util", "./tests", "./scripts", "**/*.stories.*"]
"exclude": ["./figma", "./util", "./tests", "./scripts", "**/*.stories.*"],
"include": ["./src"]
}
+3 -2
View File
@@ -24,13 +24,14 @@
"/cjs",
"/esm",
"/src",
"_docs.json"
"_docs.json",
"!**/*.tsbuildinfo"
],
"scripts": {
"docgen": "yarn ts-node ../../../scripts/docgen.ts",
"create-package-json-pointers-to-esm": "node ../../../scripts/createPackageJsonsWithESMPointers.js",
"clean": "rimraf cjs esm",
"build": "yarn run clean && concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && yarn create-package-json-pointers-to-esm",
"build": "concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && yarn create-package-json-pointers-to-esm",
"watch": "tsc --watch -p tsconfig.esm.json"
},
"dependencies": {
+2 -1
View File
@@ -3,5 +3,6 @@
"compilerOptions": {
"outDir": "./cjs",
"module": "commonjs"
}
},
"include": ["./src"]
}
+2 -8
View File
@@ -1,16 +1,10 @@
Font License
The Source Sans Pro font files in packages/node_modules/nav-frontend-typografi-style/assets are a subset of Source Sans Pro, licensed under the SIL Open Font License, and copyright Adobe Systems Incorporated, with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
Icon License
This project uses Streamline Icons. If you use nav-frontend-moduler in your project please adhere to the Streamline Icons license agreement found here: https://streamlineicons.com/ux/extended-license.html
The rest of the codebase (excluding 3rd party dependencies):
SIL Open Font License (OFL)
# The MIT License
Copyright 2019 NAV (Arbeids- og velferdsdirektoratet) - The Norwegian Labour and Welfare Administration
Copyright 2022 NAV (Arbeids- og velferdsdirektoratet) - The Norwegian Labour and Welfare Administration
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
+4 -6
View File
@@ -36,6 +36,10 @@
├── internal/css # @navikt/ds-css-internal, css interne flater
├── icons # @navikt/ds-icons, ikonpakke
└── codemod # @navikt/ds-codemod, migrerings-scripts
aksel.nav.no/
├── /website # NEXT.JS app (v12)
└── /sanity # Sanity CMS-app (v2)
```
## Commands
@@ -54,12 +58,6 @@ yarn && yarn boot
| `yarn clean` | Fjerner genererte filer |
| `yarn docgen` | Generer ts-docs |
## Dokumentasjon
[Dokumentasjon forvaltes i navikt/aksel-website](https://github.com/navikt/aksel-website)
[Migreringer](https://aksel.nav.no/designsystem/side/migrering#beta-til-v1)
## Bidra
Vi tar gjerne i mot PR-er [CONTRIBUTING.md](https://github.com/navikt/Designsystemet/blob/master/CONTRIBUTING.md)
+17
View File
@@ -0,0 +1,17 @@
FROM node:16.4-alpine3.14
RUN apk add --no-cache libc6-compat
ENV NODE_ENV production
WORKDIR /app
COPY aksel.nav.no/website/package.json .
COPY aksel.nav.no/website/.next/ .next/
COPY aksel.nav.no/website/public/ public
COPY aksel.nav.no/website/.env .
COPY aksel.nav.no/website/redirects.json .
COPY aksel.nav.no/website/next.config.js .
COPY node_modules/ node_modules/
COPY @navikt/ @navikt/
EXPOSE 3000
CMD yarn start
+92
View File
@@ -0,0 +1,92 @@
CMS og nettside for [aksel.nav.no](https://aksel.nav.no/)
```sh
├── /website # NEXT.JS app (v12)
└── /sanity # Sanity CMS-app (v2)
```
### /website
[Nextjs](https://nextjs.org/) applikasjon som henter data fra et [sanity-datasett](https://www.sanity.io/).
Siden leverer to løsninger:
- Innhold på toppnivå i Aksel: artikler, prinsipper og blogger
- Designsystem-dokumentasjon under https://aksel.nav.no/designsystem
### /sanity
Det publiserte studioet finner man på [aksel.nav.no/studio](https://aksel.nav.no/studio)(krever NAV SSO eller invite).
`production`-datasettet er privat, så man må ha riktige tilganger for å kunne lese/jobbe med dataen der.
## Kom i gang
### Bidrag
Branch `master` blir pusher rett til prod -> aksel.nav.no. For å utvikle nye features brukes branch `next` som ved push publiseres til devmiljø aksel.dev.nav.no (krever naisdevice).
### Nettside (localhost:3000)
```
yarn install
yarn workspace aksel.nav.no dev
```
**Rettigheter**
Sanity-datasettet er privat, noe som betyr du må ha tilgang til sanity applikasjonen + følgende token for å få data lokalt:
```
SANITY_PREVIEW_TOKEN
SANITY_PRIVATE_NO_DRAFTS
```
### Sanity (localhost:3333)
> !! Må være developer i https://verktoykasse.sanity.studio, Ta kontakt med team Aksel om du trenger tilgang !!
Husk at alle endringer i datasettet lokalt også gjøres i prod. Bytt derfor til `development`-dataset hvis man vil teste destruktive handlinger. Dette kan endres i `/sanity/schema.json`
```
yarn install
yarn workspace aksel.nav.no sanity
```
**Deploy av ny sanity-oppdatering**
```
yarn workspace aksel.nav.no sanity:deploy
```
### .env
Blir brukt flere keys i .env under `./website`, men kun `SANITY_PRIVATE_NO_DRAFTS` trengs for å teste lokalt
- SANITY_WRITE_KEY:
For å sende dokumenter til sanity, brukt til å oppdatere sandboxes/examples/farger. Trengs bare hvis kode skal synkes fra lokal branch
- ALGOLIA_ADMIN:
For å oppdatere records i søkemotoren. Trengs bare hvis man oppdaterer søkeindexen lokalt.
- ALGOLIA_ID:
Søkemotor index-id for å redigere riktig Index. Trengs bare for at søk skal fungere lokalt
- TRELLO_TOKEN: Sender feedback til et egent trello-board. Ikke nødvendig lokalt hvis man ikke vil teste feedback-modulen.
- SANITY_PREVIEW_TOKEN: Gir appen tilgang til å lese draft innhold fra sanity i "preview"-mode. Trengs for å teste preview-sider
- SANITY_PRIVATE_NO_DRAFTS: Gir appen tilgang til å lese innhold fra Sanity da datasettet er privat
### Søk
Søk-index blir generert av [algolia-scraper](https://github.com/algolia/docsearch-scraper). Kjøres daglig i en github-action, men kan kjøres lokalt også med docker.
```
docker run -it --env-file=.env -e "CONFIG=$(cat crawler.json | jq -r tostring)" algolia/docsearch-scraper
```
## Backups
Kjøres backups hver 3 dag til GCP-bucket under `designsystem-prod`
## Henvendelser
Spørsmål og kontakt kan rettes til team Aksel
Aksel har også en egen slack-kanal #aksel
+48
View File
@@ -0,0 +1,48 @@
{
"index_name": "aksel_docsearch",
"start_urls": [
"https://aksel.nav.no/",
{
"url": "https://aksel.nav.no/designsystem",
"tags": ["designsystem", "docs"]
},
{
"url": "https://aksel.nav.no/artikkel/",
"tags": ["aksel"],
"selectors_key": "aksel"
},
{
"url": "https://aksel.nav.no/blogg/",
"tags": ["aksel"],
"selectors_key": "aksel"
}
],
"selectors": {
"default": {
"lvl0": {
"selector": "h1.algolia-index-lvl1",
"global": true
},
"lvl1": ".algolia-index-tabs--active",
"lvl2": "h2.algolia-index-lvl2",
"lvl3": "h3.algolia-index-lvl3",
"text": "p.algolia-index-body, p.algolia-index-ingress"
},
"aksel": {
"lvl0": {
"selector": "h1.algolia-index-lvl1",
"global": true
},
"lvl1": "h2.algolia-index-lvl2",
"lvl2": "h3.algolia-index-lvl3",
"text": "p.algolia-index-body, p.algolia-index-ingress"
}
},
"selectors_exclude": [".algolia-ignore-index"],
"stop_urls": [
"https://aksel.nav.no/preview",
"https://old-design-nav.vercel.app"
],
"sitemap_urls": ["https://aksel.nav.no/api/sitemap"],
"js_render": true
}
+6
View File
@@ -0,0 +1,6 @@
AZURE_APP_CLIENT_ID=notfound
AZURE_OPENID_CONFIG_TOKEN_ENDPOINT=https://fakedings.dev-gcp.nais.io/fake/aad?NAVIdent=A123456&aud=testbruker-aksel" >> website/.env
AZURE_APP_CLIENT_SECRET=secret
AZURE_OPENID_CONFIG_ISSUER=https://fakedings.dev-gcp.nais.io/fake
AZURE_OPENID_CONFIG_JWKS_URI=https://fakedings.dev-gcp.nais.io/fake/jwks
AZURE_APP_JWK=https://fakedings.dev-gcp.nais.io/fake/jwk
+49
View File
@@ -0,0 +1,49 @@
apiVersion: "nais.io/v1alpha1"
kind: "Application"
metadata:
name: verktoykasse
namespace: designsystem
labels:
team: designsystem
spec:
image: {{ image }}
port: 3000
ingresses:
- "https://aksel.dev.nav.no"
liveness:
path: /api/isAlive
initialDelay: 20
timeout: 1
periodSeconds: 5
failureThreshold: 10
readiness:
path: /api/isAlive
initialDelay: 20
timeout: 1
replicas:
min: 1
max: 1
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
accessPolicy:
outbound:
external:
- host: api.sanity.io
- host: hnbe3yhs.api.sanity.io
- host: apicdn.sanity.io
- host: hnbe3yhs.apicdn.sanity.io
- host: cdn.sanity.io
rules: []
env:
azure:
application:
enabled: true
allowAllUsers: true
tenant: nav.no
sidecar:
enabled: true
+55
View File
@@ -0,0 +1,55 @@
apiVersion: "nais.io/v1alpha1"
kind: "Application"
metadata:
name: verktoykasse
namespace: designsystem
labels:
team: designsystem
spec:
image: {{ image }}
port: 3000
ingresses:
- "https://aksel.nav.no"
- "https://design.nav.no"
liveness:
path: /api/isAlive
initialDelay: 20
timeout: 1
periodSeconds: 5
failureThreshold: 10
readiness:
path: /api/isAlive
initialDelay: 20
timeout: 1
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
accessPolicy:
outbound:
external:
- host: api.sanity.io
- host: hnbe3yhs.api.sanity.io
- host: apicdn.sanity.io
- host: hnbe3yhs.apicdn.sanity.io
- host: cdn.sanity.io
rules: []
env:
azure:
application:
enabled: true
allowAllUsers: true
tenant: nav.no
sidecar:
enabled: true
gcp:
buckets:
- name: aksel-website-prod
retentionPeriodDays: 30
lifecycleCondition:
age: 7
numNewerVersions: 3
withState: ANY
+24
View File
@@ -0,0 +1,24 @@
{
"name": "aksel.nav.no",
"version": "1.0.0",
"private": true,
"description": "Nettside for Aksel",
"scripts": {
"dev": "yarn workspace website dev",
"build:next": "yarn workspace website build",
"serve:next": "yarn workspace website start",
"sanity": "yarn workspace sanity start",
"sanity:deploy": "yarn workspace sanity deploy",
"sanity:backup": "yarn workspace sanity backup",
"sanity:update": "yarn workspace website update:sanity",
"test": "yarn workspace website test"
},
"workspaces": [
"sanity",
"website"
],
"devDependencies": {
"@babel/core": "^7.0.0",
"prettier-plugin-tailwindcss": "^0.1.13"
}
}
@@ -0,0 +1,48 @@
// https://www.sanity.io/schemas/make-a-field-read-only-after-publishing-once-d87bf5f6
import { useState, useEffect } from "react";
import { useDocumentOperation, useValidationStatus } from "@sanity/react-hooks";
import PublishIcon from "part:@sanity/base/publish-icon";
export default function SetAndPublishAction(props) {
const { patch, publish }: any = useDocumentOperation(props.id, props.type);
const { isValidating, markers }: any = useValidationStatus(
props.id,
props.type
);
const [isPublishing, setIsPublishing] = useState(false);
useEffect(() => {
// if the isPublishing state was set to true and the draft has changed
// to become `null` the document has been published
if (isPublishing && !props.draft) {
setIsPublishing(false);
}
}, [props.draft]);
console.log(markers);
return {
disabled:
publish.disabled ||
isValidating ||
markers.some((x) => x?.level === "error"),
label: isPublishing ? "Publishing…" : "Publish",
color: "success",
icon: PublishIcon,
onHandle: () => {
// This will update the button text
setIsPublishing(true);
// Set publishedAt to current date and time
!props.published &&
patch.execute([{ set: { publishedAt: new Date().toISOString() } }]);
// Perform the publish
publish.execute();
// Signal that the action is completed
props.onComplete();
},
};
}
@@ -0,0 +1,139 @@
import { EyeScreened } from "@navikt/ds-icons";
import { BodyLong, Button } from "@navikt/ds-react";
import React from "react";
const sanityClient = require("@sanity/client");
const SanityConfig = require("../../sanity.json");
const client = sanityClient({
projectId: SanityConfig.api.projectId,
dataset: SanityConfig.api.dataset,
apiVersion: "2021-08-21",
withCredentials: true,
});
export default function SetPrivateAction(props) {
const [isDialogOpen, setDialogOpen] = React.useState(false);
const isPrivate =
props?.published?.tilgang?.privat ?? props?.draft?.tilgang?.privat;
const handlePatch = async (dir: boolean) => {
props?.published &&
(await client
.patch(props.id)
.set({ tilgang: { privat: dir } })
.commit()
.catch((err) => {
console.error(err.message);
}));
props?.draft &&
(await client
.patch(props.draft._id)
.set({ tilgang: { privat: dir } })
.commit()
.catch((err) => {
console.error(err.message);
}));
props.onComplete();
};
return {
label: isPrivate ? "Åpne side for alle" : "Skjul side bak innlogging",
/* color: "success",
icon: PublishIcon, */
icon: EyeScreened,
onHandle: async () => {
setDialogOpen(true);
},
dialog: isDialogOpen && {
type: "modal",
onClose: props.onComplete,
header: isPrivate
? "Åpne innhold for alle"
: "Gjemme innhold bak innlogging",
content: (
<div>
{isPrivate ? (
<BodyLong spacing>
Innholdet gjøres nå tilgjengelig for alle utenfor NAV! Dette kan
medføre konsekvenser siden innholdet i artikkelen blir
tilgjengelig for alle. Innhold som NAV potensielt ikke ønsker å
dele offentlig kan medføre eks omdømmetap og uønsket oppmerksomhet
utenfra. Dobbelsjekk med temaansvarlig og andre som har skrevet
artikkelen før du velger å åpne innholdet for alle. Er du usikker
er det bare å sende en melding på #aksel eller #aksel-redaktører
på slack.
</BodyLong>
) : (
<>
<BodyLong spacing>
Innholdet gjøres nå bare tilgjengelig for NAV-ansatte. Alt
utenom "innhold" vil fortsatt være tilgjengelig for alle. Dette
inkluderer alle bilder som er lastet opp til sanity også! Husk
at ingen "sikker" data skal skrives i Aksel, og at
innloggings-barrieren er ment for å kunne dele innhold som vi
ikke er helt trygg på å dele med alle.
</BodyLong>
Dette vil fortsatt være åpent for alle
<ul>
<li>Sidetittel</li>
<li>Ingress</li>
<li>Redaktører</li>
<li>Bilder/filer</li>
<li>SEO</li>
</ul>
Dette vil bare være tilgjengelig for NAV-ansatte
<ul>
<li>"Innhold"-blokken</li>
</ul>
</>
)}
<div style={{ display: "flex", gap: "6px", marginTop: 8 }}>
{isPrivate ? (
<>
<Button
onClick={() => {
setDialogOpen(false);
handlePatch(false);
}}
>
Åpne innholdet for alle
</Button>
<Button
variant="secondary"
onClick={() => {
setDialogOpen(false);
props.onComplete();
}}
>
Avbryt
</Button>
</>
) : (
<>
<Button
onClick={() => {
setDialogOpen(false);
handlePatch(true);
}}
>
Skjul innholdet bak innlogging
</Button>
<Button
variant="secondary"
onClick={() => {
setDialogOpen(false);
props.onComplete();
}}
>
Avbryt
</Button>
</>
)}
</div>
</div>
),
},
};
}
@@ -0,0 +1,3 @@
import MediaAssetSource from "part:sanity-plugin-media/asset-source";
import Unsplash from "part:sanity-plugin-asset-source-unsplash/image-asset-source";
export default [MediaAssetSource, Unsplash];
@@ -0,0 +1,52 @@
import { useEffect } from "react";
import config from "config:sanity";
const BUNDLE_CHECK_INTERVAL = 60 * 1000;
const BUNDLE_CHECK_INTERVAL_DECLINED = 60 * 10000;
const CHANGES_AVAILABLE_MESSAGE =
"Studioet er har nye endringer! For å ta i bruk disse vil siden nå oppdateres for den nyeste versjonen.";
async function getCurrentHash() {
const basePath = (config.project && config.project.basePath) || "/";
const html = await window.fetch(basePath).then((res) => res.text());
const [, hash] = html.match(/app\.bundle\.js\?(\w+)/) || [];
return hash;
}
let hash = null;
let interval = null;
const BundleChecker = () => {
useEffect(() => {
getCurrentHash().then((newHash) => {
hash = newHash;
});
interval = createInterval();
return () => clearInterval(interval);
}, []);
// We're a react component, in theory, so return null to not render anything
return null;
};
export default BundleChecker;
const createInterval = (declined?: boolean) =>
setInterval(
async () => {
const newHash = await getCurrentHash();
if (hash && newHash !== hash) {
clearInterval(interval);
if (window.confirm(CHANGES_AVAILABLE_MESSAGE)) {
window.location.reload();
} else {
interval = createInterval(true);
}
}
},
declined ? BUNDLE_CHECK_INTERVAL_DECLINED : BUNDLE_CHECK_INTERVAL
);
@@ -0,0 +1,44 @@
import { allDocumentTypes } from "../config";
export default {
widgets: [
{
name: "document-list",
options: {
title: "Drafts for God Praksis",
query: '*[_type in $types && (_id in path("drafts.**"))]',
queryParams: {
types: allDocumentTypes.filter((x) => x.startsWith("gp")),
},
},
},
{
name: "document-list",
options: {
title: "Drafts for Designsystemet",
query: '*[_type in $types && (_id in path("drafts.**"))]',
queryParams: {
types: allDocumentTypes.filter((x) => x.startsWith("ds")),
},
},
},
{
name: "outdated-documents-widget",
options: {
type: "warning",
},
},
{
name: "outdated-documents-widget",
options: {
type: "error",
},
},
{
name: "project-info",
},
{
name: "project-users",
},
],
};
@@ -0,0 +1,23 @@
import { Picture } from "@navikt/ds-icons";
import S from "@sanity/desk-tool/structure-builder";
import React from "react";
export const adminPanel = async (roles) =>
roles.find((x) => x.name === "administrator") &&
S.listItem()
.title("Admin")
.child(
S.list()
.title("Admin")
.items([
S.documentListItem()
.title(`Forside Aksel`)
.schemaType(`vk_frontpage`)
.icon(() => <Picture />)
.id(`frontpage_vk_praksis`),
S.listItem()
.title("Standalone-sider")
.child(S.documentTypeList("aksel_standalone")),
])
);
@@ -0,0 +1,102 @@
import { prinsipper } from "@/lib";
import { Facilitet } from "@navikt/ds-icons";
import S from "@sanity/desk-tool/structure-builder";
import documentStore from "part:@sanity/base/datastore/document";
import React from "react";
import { map } from "rxjs/operators";
const temaQuery = `*[_type == "aksel_tema" && count(*[references(^._id)]) > 0]`;
const AkselLogo = () => (
<svg
width="192px"
height="192px"
viewBox="0 0 192 192"
fill="none"
xmlns="http://www.w3.org/2000/svg"
focusable="false"
aria-hidden="true"
strokeLinecap="round"
strokeWidth="10"
stroke="currentColor"
>
<path d="m41.63 150.3 47.39-47.38" />
<path d="M40.92 80.3 77.7 43.53c4.25-4.24 9.9-4.24 14.15 0l56.56 56.57c4.25 4.25 4.25 9.9 0 14.14l-36.77 36.77" />
<path d="m40.92 80.3 38.19-38.18c3.53-3.54 6.72-3.18 13.79-3.18h48.79s0 0 0 0c6.36 0 11.31 4.95 11.31 11.31 0 0 0 0 0 0v48.8c0 7.06.35 10.24-3.18 13.78l-38.19 38.18" />
</svg>
);
export const akselInnhold = async () => {
return S.listItem()
.title("Aksel")
.icon(() => <AkselLogo />)
.child(
S.list()
.title("Innhold")
.items([
S.listItem()
.title("Artikler")
.child(() =>
documentStore.listenQuery(temaQuery).pipe(
map((tema: any) => {
return S.list()
.title("Artikler")
.items([
S.listItem()
.title("Alle artikler")
.child(S.documentTypeList("aksel_artikkel")),
S.listItem()
.title("Uten tema")
.child(
S.documentTypeList("aksel_artikkel").filter(
`_type == "aksel_artikkel" && !defined(tema)`
)
),
...tema.map((tag) => {
return S.listItem()
.title(tag.title)
.child(
S.documentList()
.title(tag.title)
.filter(
`_type == 'aksel_artikkel' && $tag in tema[]->title`
)
.params({ tag: tag.title })
);
}),
]);
})
)
),
S.listItem().title("Blogg").child(S.documentTypeList("aksel_blogg")),
S.listItem()
.title("Prinsipper")
.child(
S.list()
.title("Prinsipper")
.items([
S.listItem()
.title("Alle artikler")
.child(S.documentTypeList("aksel_prinsipp")),
...prinsipper.map(({ value, title }) =>
S.listItem()
.title(title)
.child(
S.documentList()
.title(title)
.filter(
`_type == 'aksel_prinsipp' && $value == prinsipp.prinsippvalg`
)
.params({ value })
)
),
])
),
S.divider(),
S.listItem()
.title("Temasider")
.icon(() => <Facilitet />)
.child(S.documentTypeList("aksel_tema")),
])
);
};
@@ -0,0 +1,92 @@
import { CoApplicant, Facilitet, Information, People } from "@navikt/ds-icons";
import S from "@sanity/desk-tool/structure-builder";
import userStore from "part:@sanity/base/user";
import React from "react";
import { createSuperPane } from "sanity-super-pane";
import { ComponentPageWebPreview } from "../../web-previews/ComponentWebPreview";
import { KomponentPreview } from "../../web-previews/KomponentPreview";
import { PageWebPreview } from "../../web-previews/PageWebPreview";
import { adminPanel } from "./admin";
import { akselInnhold } from "./aksel";
import { dsPanel } from "./ds";
import { profilePanel } from "./profile";
export const getDefaultDocumentNode = ({ schemaType }) => {
switch (schemaType) {
case "komponent_artikkel":
return S.document().views([
S.view.form(),
S.view.component(ComponentPageWebPreview).title("Preview"),
S.view.component(KomponentPreview).title("Preview-dev"),
]);
case "ds_artikkel":
return S.document().views([
S.view.form(),
S.view.component(PageWebPreview).title("Preview"),
S.view.component(KomponentPreview).title("Preview-dev"),
]);
case "aksel_artikkel":
return S.document().views([
S.view.form(),
S.view.component(PageWebPreview).title("Preview"),
S.view.component(KomponentPreview).title("Preview-dev"),
]);
case "aksel_blogg":
return S.document().views([
S.view.form(),
S.view.component(PageWebPreview).title("Preview"),
S.view.component(KomponentPreview).title("Preview-dev"),
]);
case "aksel_prinsipp":
return S.document().views([
S.view.form(),
S.view.component(PageWebPreview).title("Preview"),
S.view.component(KomponentPreview).title("Preview-dev"),
]);
case "aksel_standalone":
return S.document().views([
S.view.form(),
S.view.component(PageWebPreview).title("Preview"),
S.view.component(KomponentPreview).title("Preview-dev"),
]);
}
};
const items = [
S.divider(),
S.listItem()
.title("Redaktører")
.icon(() => <People />)
.child(createSuperPane("editor")),
S.divider(),
];
export default () => {
return userStore.getCurrentUser().then(async ({ roles, id }) => {
const panels = [];
const admin = await adminPanel(roles);
const profile = await profilePanel(id);
const ds = await dsPanel(roles);
const aksel = await akselInnhold();
const intro = [];
profile && intro.push(profile);
profile && intro.push(S.divider());
let struct = items;
if (ds) {
struct = [ds, ...struct];
}
struct = [aksel, ...struct];
if (admin) {
struct = [...struct, admin];
}
return S.list()
.title("Aksel")
.items([...intro, ...struct, ...panels]);
});
};
@@ -0,0 +1,119 @@
import {
Facilitet,
FileContent,
Findout,
Folder,
Picture,
Place,
Ruler,
Task,
} from "@navikt/ds-icons";
import S from "@sanity/desk-tool/structure-builder";
import client from "part:@sanity/base/client";
import React from "react";
import { createSuperPane } from "sanity-super-pane";
export const dsPanel = async (roles) => {
const role = roles.some(({ name }) => "aksel-editor" === name);
if (role) {
return null;
} else {
return S.listItem()
.title("Designsystemet")
.icon(() => <Facilitet />)
.child(
S.list()
.title("Designsystem")
.items([
S.listItem()
.title("Komponentsider")
.icon(() => <Facilitet />)
.child(createSuperPane("komponent_artikkel")),
S.listItem()
.title("Artikler")
.icon(() => <FileContent />)
.child(createSuperPane("ds_artikkel")),
S.divider(),
S.documentListItem()
.title(`Navigasjon`)
.schemaType(`ds_navigation`)
.icon(() => <Place />)
.id(`ds_navigationid`),
S.listItem()
.title("Publiserte sider ikke i navigasjon")
.child(async (): Promise<any> => {
const doc = await client.fetch(
`*[_id == 'ds_navigationid'][0]{
headings[]{
title,
menu
}
}`
);
if (!doc.headings) {
return [];
}
let allIds = "";
try {
allIds = doc.headings
.map((heading) =>
heading.menu
.filter((item) => item._type === "item")
.map((item) => item.link._ref)
.map((x) => `"${x}"`)
.join(",")
)
.join(",");
} catch (e) {
console.log(e);
}
return S.documentList()
.title("Sider ikke i navigasjon")
.filter(
`!(_id in [${allIds}]) && !(_id in path('drafts.**')) && _type in ["komponent_artikkel", "ds_artikkel"]`
);
}),
S.documentListItem()
.title(`Forside`)
.schemaType(`ds_frontpage`)
.icon(() => <Picture />)
.id(`frontpage_designsystem`),
S.divider(),
S.listItem()
.title("Autogenerert data")
.child(
S.list()
.title("Autogenerert data")
.items([
S.listItem()
.title("Eksempler")
.icon(() => <span>{`EX`}</span>)
.child(S.documentTypeList("kode_eksempler_fil")),
S.listItem()
.title("Fargekategorier")
.icon(() => <Folder />)
.child(S.documentTypeList("ds_color_categories")),
S.listItem()
.title("Props")
.icon(() => <Folder />)
.child(S.documentTypeList("ds_props")),
S.listItem()
.title("Tokens")
.icon(() => <Folder />)
.child(S.documentTypeList("ds_tokens")),
])
),
S.divider(),
S.documentListItem()
.title(`Komponent-template`)
.schemaType(`ds_component_template`)
.icon(() => <Task />)
.id(`ds_component_templateid`),
])
);
}
};
@@ -0,0 +1,26 @@
import { Picture } from "@navikt/ds-icons";
import S from "@sanity/desk-tool/structure-builder";
import userStore from "part:@sanity/base/user";
import client from "part:@sanity/base/client";
import React from "react";
export const profilePanel = async (id) => {
const ids = await client.withConfig({ apiVersion: "2021-05-31" })
.fetch(`*[_type == "editor"]{
_id,
user_id
}`);
const editor = ids.find(({ user_id }) => user_id?.current === id);
if (editor) {
return S.documentListItem()
.title(`Profil`)
.schemaType(`editor`)
.icon(() => <Picture />)
.id(editor._id);
} else {
return null;
}
};
@@ -0,0 +1,53 @@
import React from "react";
import { People } from "@navikt/ds-icons";
import T from "@sanity/base/initial-value-template-builder";
import userStore from "part:@sanity/base/user";
const sanityClient = require("@sanity/client");
const sanityToken = process.env.SANITY_TOKEN;
const SanityConfig = require("../sanity.json");
const client = sanityClient({
projectId: SanityConfig.api.projectId,
dataset: SanityConfig.api.dataset,
apiVersion: "2020-06-19",
/* Token er safe og har bare tillatelse til å lese template doc */
token:
"skt6kqEcXmRxUTGDzdEcw11g7HJi9DaHX2fyn8Z0Dearr1k67ykW2N64KTP9GxAPNN6P8TlzPjUdkfGsvbJkfmwfq0UNudKfMYOfyz9OeA666y15pPLRr6D7xnrJGEEBYwSvG6baLLiJI478YowWpjqp1Mz2U1PbAPnIMW4W5btTpdQbwPSD",
useCdn: false,
});
export default [
...T.defaults().filter((x) => !["komponent_artikkel"].includes(x.spec.id)),
T.template({
id: "komponent_artikkel_template",
title: "Mal for Komponentsider",
schemaType: "komponent_artikkel",
value: async () => {
const doc = await client.fetch(`*[_type == "ds_component_template"][0]`);
return doc
? Object.entries(doc).reduce((old, [key, val]) => {
return key.startsWith("_") ? { ...old } : { ...old, [key]: val };
}, {})
: {};
},
}),
T.template({
id: "newEditor",
title: "Editor",
schemaType: "editor",
icon: () => <People />,
value: async () => {
const { id, displayName } = await userStore.getUser("me");
return {
title: displayName,
user_id: {
current: id,
},
_type: "editor",
};
},
}),
];
@@ -0,0 +1,38 @@
import S from "@sanity/base/structure-builder";
const docTypes = [
{
name: "aksel_artikkel",
title: "Aksel Artikkel",
},
{
name: "newEditor",
title: "Bruker/redaktør",
desc: "Lager en psuedo-bruker for deg som kan koble artikler til brukeren",
},
{
name: "aksel_blogg",
title: "Bloggpost",
},
{
name: "ds_artikkel",
title: "Designsystem Artikkel",
desc: "Atikkel som legges under designsystemet",
},
{
name: "komponent_artikkel_template",
title: "Designsystem Komponentside",
desc: "Atikkel som legges under designsystemet",
},
];
export default [
...docTypes.map((doct) =>
S.initialValueTemplateItem(doct.name)
.id(`doc-${doct.name}`)
.title(doct.title)
.templateId(doct.name)
.description(doct?.desc ?? "")
),
/* ...S.defaultInitialValueTemplateItems() */
];
@@ -0,0 +1,15 @@
import defaultResolve, {
PublishAction,
} from "part:@sanity/base/document-actions";
import SetAndPublishAction from "./actions/publishedAt";
/* import SetPrivateAction from "./actions/set-private"; */
import { allDocumentTypes } from "../config";
export default function resolveDocumentActions(props) {
const [publish, ...rest] = defaultResolve(props).map((Action) => {
return allDocumentTypes.includes(props.type) && Action === PublishAction
? SetAndPublishAction
: Action;
});
return [publish /* SetPrivateAction */, ...rest];
}
@@ -0,0 +1,53 @@
import { Button, Card, Inline, Stack, Text } from "@sanity/ui";
import moment from "moment";
import PatchEvent, { set } from "part:@sanity/form-builder/patch-event";
import React from "react";
const CustomDisplay = React.forwardRef((props, ref) => {
const click = () => {
props.onChange &&
props.onChange(
PatchEvent.from(set(new Date().toISOString().split("T")[0]))
);
};
const lastUpdate = moment(props.value);
const daysSince = moment().diff(lastUpdate, "days");
return (
<Stack tabIndex={0} space={4} ref={ref}>
{daysSince > 90 && (
<Card
padding={[3, 3, 4]}
radius={2}
shadow={1}
tone={daysSince > 180 ? "critical" : "caution"}
>
<Text align="center" size={[2, 2, 3]}>
{daysSince > 180
? "Innholdet er utdatert!"
: "Innholdet er stagnert!"}
</Text>
</Card>
)}
<Stack space={4}>
<Text size={1} weight="semibold">
Sist oppdatert
</Text>
<Text size={2}>{props.value?.split("-").reverse().join("/")}</Text>
</Stack>
<Inline space={[3, 3, 4]}>
<Button
onClick={() => click()}
fontSize={[2]}
mode="ghost"
padding={[2, 2]}
text="Oppdater"
/>
</Inline>
</Stack>
);
});
export default CustomDisplay;
@@ -0,0 +1,30 @@
import React, { useState, useEffect, useRef } from "react";
import { withDocument } from "part:@sanity/form-builder";
import {
TextInput,
Stack,
Label,
Checkbox,
Radio,
Flex,
Text,
Box,
Card,
} from "@sanity/ui";
import styled from "styled-components";
/* TODO: Implement this. Issue now is that document returns null in props */
const CustomDisplay = React.forwardRef((props, ref) => {
const [doc, setDoc] = useState(null);
useEffect(() => {
doc && setDoc(props.document);
}, [props]);
return (
<Stack space={0} ref={ref}>
<Card padding={0}>T</Card>
</Stack>
);
});
export default withDocument(CustomDisplay);
@@ -0,0 +1,49 @@
import React from "react";
import Preview from "part:@sanity/base/preview";
import { IntentLink } from "part:@sanity/base/router";
import schema from "part:@sanity/base/schema";
import {
Item as DefaultItem,
List as DefaultList,
} from "part:@sanity/components/lists/default";
import styles from "./profile.css";
const ReferringDocumentsList = (props) => {
const { documents } = props;
return (
<DefaultList className={styles.root}>
{documents
.sort(
(a, b) =>
new Date(b._updatedAt).getTime() - new Date(a._updatedAt).getTime()
)
.map((document) => {
const schemaType = schema.get(document._type);
return (
<DefaultItem key={document._id} className={styles.item}>
{schemaType ? (
<IntentLink
className={styles.link}
intent="edit"
params={{ id: document._id, type: document._type }}
>
<Preview value={document} type={schemaType} />
{document._updatedAt
.split("T")[0]
.split("-")
.reverse()
.join("-")}
</IntentLink>
) : (
<div>
A document of the unknown type <em>{document._type}</em>
</div>
)}
</DefaultItem>
);
})}
</DefaultList>
);
};
export default ReferringDocumentsList;
@@ -0,0 +1,18 @@
export const getUnpublished = (docs: Record<string, any>[]) =>
docs.filter(
(doc, i, arr) =>
doc._id.startsWith("drafts.") &&
!arr.find((x, y) => i !== y && doc._id.includes(x._id))
);
export const getDrafts = (docs: Record<string, any>[]) =>
docs.filter((doc, i, arr) =>
arr.find((x, y) => i !== y && x._id.includes(doc._id))
);
export const getPublished = (docs: Record<string, any>[]) =>
docs
.filter(
(doc, i, arr) => !arr.find((x, y) => i !== y && x._id.includes(doc._id))
)
.filter((doc) => !doc._id.includes("drafts"));
@@ -0,0 +1,171 @@
import { Caseworker, Sandglass, Upload, Wrench } from "@navikt/ds-icons";
import {
BodyShort,
Detail,
Heading,
HelpText,
Label,
Link,
ReadMore,
} from "@navikt/ds-react";
import { SanityDocument } from "@sanity/client";
import { Stack } from "@sanity/ui";
import userStore from "part:@sanity/base/user";
import { WithReferringDocuments } from "part:@sanity/base/with-referring-documents";
import Spinner from "part:@sanity/components/loading/spinner";
import { withDocument } from "part:@sanity/form-builder";
import React, { useEffect, useState } from "react";
import ReferringDocumentsList from "./document-list";
import styles from "../styles.css";
import { getDrafts, getPublished, getUnpublished } from "./filter-docs";
type Props = {
referringDocuments: Record<string, any>[];
isLoading: boolean;
published?: SanityDocument | null;
type: any;
document: any;
};
const IntroPage = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
const { document } = props;
const [user, setUser] = useState<any>({});
useEffect(() => {
userStore.getCurrentUser().then((user) => setUser(user));
}, []);
const EmptyState = () => (
<Detail className={styles.detailsContent} size="small">
Ingen sider her...
</Detail>
);
if (
!document._id ||
(document?.user_id?.current !== user.id &&
!user?.roles?.find((x) => x.name === "administrator"))
)
return null;
return (
<Stack ref={ref} space={[3, 1, 2, 4]}>
<Heading level="2" size="medium" spacing style={{ display: "flex" }}>
Profil <Caseworker style={{ marginLeft: "0.5rem", fontSize: "2rem" }} />
</Heading>
{user?.roles && (
<div>
<Heading level="3" size="small" spacing>
Roller/tilganger:
</Heading>
{user?.roles.map((x, i) => (
<div key={i}>
<Label size="small" spacing as="p">
{x?.title}
</Label>
<BodyShort size="small">{x?.description}</BodyShort>
</div>
))}
</div>
)}
<BodyShort
size="small"
spacing
as="span"
style={{ display: "flex", gap: "0.5rem" }}
>
<ReadMore header="Har du ikke de rollene du trenger?">
Send en melding til oss på{" "}
<Link href="https://nav-it.slack.com/archives/C0370ADS0HX">
#Aksel
</Link>{" "}
slack så fikser vi det.
</ReadMore>
</BodyShort>
<div style={{ borderTop: "1px solid #bbb" }} />
<WithReferringDocuments id={document._id?.replace("drafts.", "")}>
{({ referringDocuments, isLoading }) => {
if (isLoading) {
return <Spinner message="Looking for referring documents…" />;
}
if (!referringDocuments.length) return null;
const drafts = getDrafts(referringDocuments);
const published = getPublished(referringDocuments);
const unpublished = getUnpublished(referringDocuments);
return (
<Stack space={2} className={styles.profileDetails}>
<Heading level="3" size="xsmall" style={{ display: "flex" }}>
<Upload />
Publiserte sider
</Heading>
<details>
<Label size="small" as="summary">
<BodyShort size="small">
Oppdaterte: {published.length}
</BodyShort>
</Label>
{published ? (
<ReferringDocumentsList documents={published} />
) : (
<EmptyState />
)}
</details>
<details>
<Label size="small" as="summary">
<BodyShort size="small">
Med upubliserte endringer: {drafts.length}
</BodyShort>
</Label>
{drafts ? (
<ReferringDocumentsList documents={drafts} />
) : (
<EmptyState />
)}
</details>
<Heading level="3" size="xsmall" className={styles.headings}>
<Wrench />
Under arbeid
</Heading>
<details>
<Label size="small" as="summary">
<BodyShort size="small">
Sider: {unpublished.length}
</BodyShort>
</Label>
{unpublished ? (
<ReferringDocumentsList documents={unpublished} />
) : (
<EmptyState />
)}
</details>
<Heading level="3" size="xsmall" className={styles.headings}>
<Sandglass />
Utdaterte sider
</Heading>
<details>
<Label size="small" as="summary">
<BodyShort size="small">
Ikke oppdatert på over 90 dager: 0
</BodyShort>
</Label>
<EmptyState />
</details>
<details>
<Label size="small" as="summary">
<BodyShort size="small">
Ikke oppdatert på over 180 dager: 0
</BodyShort>
</Label>
<EmptyState />
</details>
</Stack>
);
}}
</WithReferringDocuments>
</Stack>
);
});
export default withDocument(IntroPage);
@@ -0,0 +1,35 @@
/* ./cms/styles/ReferringDocumentsList.css */
@import "part:@sanity/base/theme/variables-style";
.root {
composes: root from "part:@sanity/components/lists/default-style";
}
.item {
composes: lineBetween from "part:@sanity/components/lists/default-item-style";
background: var(--white);
border-radius: var(--border-radius-base);
color: inherit;
margin-bottom: var(--small-padding);
border: 1px solid var(--list-border-color);
}
.item:hover {
background-color: var(--navds-semantic-color-interaction-primary-hover-subtle, rgba(230, 240, 255, 1));
}
.link {
padding: var(--small-padding);
color: inherit;
text-decoration: none;
display: flex;
align-items: center;
justify-content: space-between;
}
.link:focus {
border-radius: 2px;
outline: none;
box-shadow: var(--navds-shadow-focus, var(--navds-shadow-focus, 0 0 0 3px rgba(0, 52, 125, 1)));
}
+83
View File
@@ -0,0 +1,83 @@
@import "part:@sanity/base/theme/variables-style";
.link {
composes: item from "part:@sanity/base/theme/layout/selectable-style";
display: block;
color: inherit;
text-decoration: inherit;
outline: none;
padding: 0.5rem 0;
height: 100%;
}
.profileDetails details {
border-radius: 1px;
}
.profileDetails details summary {
cursor: pointer;
padding: 0.5rem;
padding-left: 0.66rem;
margin-bottom: 0;
border-radius: 1px;
position: relative;
list-style: none;
}
.profileDetails details summary > p {
margin-left: 1.35rem;
}
.profileDetails > * > svg {
margin-right: 0.5rem;
font-size: 1.5rem;
}
.profileDetails details summary:hover {
background-color: var(--navds-semantic-color-interaction-primary-hover-subtle, rgba(230, 240, 255, 1));
}
.profileDetails details summary:focus {
outline: none;
}
.profileDetails details summary:focus-visible {
box-shadow: var(--navds-shadow-focus, 0 0 0 3px rgba(0, 52, 125, 1));
}
.profileDetails details summary > * {
display: inline;
}
.profileDetails details summary::marker {
display: none;
content: "";
}
.profileDetails details summary::before {
color: #3b3b3b;
content: "+";
font-size: 1.5rem;
display: block;
position: absolute;
left: 0.27rem;
top: 6px;
}
.profileDetails details[open] summary::before {
content: "-";
}
.profileDetails details[open] summary {
margin-bottom: 0.75rem;
}
.headings {
margin-top: 1rem !important;
display: flex;
}
p.detailsContent {
margin-left: 2rem;
margin-bottom: 1.5rem;
}
@@ -0,0 +1,130 @@
import { Heading, Stack, Text } from "@sanity/ui";
import { withDocument } from "part:@sanity/form-builder";
import { getPublishedId } from "part:@sanity/base/util/draft-utils";
import schema from "part:@sanity/base/schema";
import React, { useEffect, useCallback, useState } from "react";
import moment from "moment";
import { IntentLink } from "part:@sanity/base/router";
import { Item, List } from "part:@sanity/components/lists/default";
import SanityPreview from "part:@sanity/base/preview";
import styles from "./styles.css";
const sanityClient = require("@sanity/client");
const SanityConfig = require("../sanity.json");
const client = sanityClient({
projectId: SanityConfig.api.projectId,
dataset: SanityConfig.api.dataset,
apiVersion: "2020-06-19",
useCdn: false,
});
const RelatedArticles = React.forwardRef((props, ref) => {
const [userDocuments, setUserDocuments] = useState([]);
const getUserDocs = useCallback(async () => {
const userDocs = await client
.fetch(`*["${props.document._id}" == contact._ref]`)
.catch((e) => console.error(e.message));
userDocs && setUserDocuments(userDocs);
}, [props.document]);
useEffect(() => {
if (!props.document) {
return;
}
getUserDocs();
}, [getUserDocs]);
const outdated = userDocuments.filter((doc) => {
if (!doc?.metadata?.last_update) return false;
const lastUpdate = moment(doc._updatedAt);
const daysSince = moment().diff(lastUpdate, "days");
return daysSince > 180;
});
const stagnated = userDocuments.filter((doc) => {
if (!doc?.metadata?.last_update) return false;
const lastUpdate = moment(doc._updatedAt);
const daysSince = moment().diff(lastUpdate, "days");
return daysSince > 90 && daysSince <= 180;
});
return (
<Stack space={0} ref={ref} space={[3, 1, 2, 4]}>
<details>
<summary>Utdaterte sider: {outdated.length}</summary>
<List>
{outdated.map((doc) => (
<Item key={doc._id}>
<IntentLink
intent="edit"
params={{
type: doc._type,
id: getPublishedId(doc._id),
}}
className={styles.link}
>
<SanityPreview
layout="default"
type={schema.get(doc._type)}
value={doc}
key={doc._id}
/>
</IntentLink>
</Item>
))}
</List>
</details>
<details>
<summary>Stagnerte sider: {stagnated.length}</summary>
<List>
{stagnated.map((doc) => (
<Item key={doc._id}>
<IntentLink
intent="edit"
params={{
type: doc._type,
id: getPublishedId(doc._id),
}}
className={styles.link}
>
<SanityPreview
layout="default"
type={schema.get(doc._type)}
value={doc}
key={doc._id}
/>
</IntentLink>
</Item>
))}
</List>
</details>
<details>
<summary>Alle sider: {userDocuments.length}</summary>
<List>
{userDocuments.map((doc) => (
<Item key={doc._id}>
<IntentLink
intent="edit"
params={{
type: doc._type,
id: getPublishedId(doc._id),
}}
className={styles.link}
>
<SanityPreview
layout="default"
type={schema.get(doc._type)}
value={doc}
key={doc._id}
/>
</IntentLink>
</Item>
))}
</List>
</details>
</Stack>
);
});
export default withDocument(RelatedArticles);
+41
View File
@@ -0,0 +1,41 @@
const teams = [
{
name: "designsystem",
prefix: "ds",
documents: ["komponent_artikkel", "ds_artikkel"],
navigation: `navigation_designsystem`,
},
{
name: "god_praksis",
prefix: "gp",
documents: ["aksel_artikkel", "aksel_blogg", "aksel_prinsipp"],
},
];
/* Collection of all document-pages to account for */
const allDocumentTypes = teams.reduce(
(docs, team) => [...docs, ...team.documents],
[]
);
/* Collection of all navigation-documents */
const allNavDocumentIds = teams.map((x) => x?.navigation).filter((x) => x);
/**
* Defines when a document of a spesific type is set to stagnant or expired
* @returns [stagnant Date, expired Date]
*/
function getExpireDates() {
const stagnantDate = new Date();
const expiredDate = new Date();
stagnantDate.setDate(stagnantDate.getDate() + 120);
expiredDate.setDate(expiredDate.getDate() + 180);
return [stagnantDate, expiredDate];
}
module.exports = {
teams,
allDocumentTypes,
allNavDocumentIds,
getExpireDates,
};
+9
View File
@@ -0,0 +1,9 @@
{
"#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!",
"@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea",
"@sanity/default-login": "6fb6d3800aa71346e1b84d95bbcaa287879456f2922372bb0294e30b968cd37f",
"@sanity/form-builder": "b38478227ba5e22c91981da4b53436df22e48ff25238a55a973ed620be5068aa",
"@sanity/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6",
"@sanity/vision": "da5b6ed712703ecd04bf4df560570c668aa95252c6bc1c41d6df1bda9b8b8f60",
"power-table": "922902575995e62af8f628e6abe652ecc5f3b1e1d2dec5859392e79e47707088"
}
@@ -0,0 +1,3 @@
{
"listOptions": {}
}
@@ -0,0 +1,6 @@
{
"toolSwitcher": {
"order": [],
"hidden": []
}
}
@@ -0,0 +1,14 @@
{
"providers": {
"mode": "append",
"redirectOnSingle": false,
"entries": [
{
"name": "saml",
"title": "NAV SSO",
"url": "https://api.sanity.io/v2021-10-01/auth/saml/login/f3270b37",
"logo": "/static/navlogo.svg"
}
]
}
}
@@ -0,0 +1,5 @@
{
"images": {
"directUploads": true
}
}
@@ -0,0 +1,3 @@
{
"defaultApiVersion": "2021-10-21"
}
@@ -0,0 +1,3 @@
{
"add-config": "here"
}
+129
View File
@@ -0,0 +1,129 @@
import { allDocumentTypes } from "../config";
export const editorField = {
title: "Redaktører",
description: "Legg til alle som har bidratt med denne siden!",
name: "contributors",
type: "array",
of: [{ type: "reference", to: [{ type: "editor" }] }],
group: "settings",
validation: (Rule) => Rule.required(),
};
export const privateField = {
title: "Sidetilganger",
name: "tilgang",
type: "object",
description: "Styrer tilgang for side-innhold",
hidden: true,
fields: [
{
title: "Privat",
name: "privat",
type: "boolean",
},
],
initialValue: {
privat: true,
},
};
export const publishedAtField = {
title: "Publiseringsdato",
name: "publishedAt",
type: "datetime",
group: "settings",
description: "Synlig bare for admins. Setter publiseringsdato for dokument",
hidden: ({ currentUser }) =>
!currentUser.roles.find((x) => x.name === "administrator"),
};
export const migratedField = {
title: "Migrert riktekst",
name: "isMigrated",
type: "boolean",
hidden: true,
initialValue: true,
};
export const titleField = {
title: "Sidetittel",
name: "heading",
type: "string",
group: "innhold",
description:
"Bruk en kort og konsis tittel om mulig. Blir satt som `<H1 />` på toppen av siden i URL.",
validation: (Rule) =>
Rule.required()
.max(60)
.error("Siden bør ha en kort og konsis heading (<h1>)"),
};
export const ingressField = {
title: "Ingress",
name: "ingress",
description: "Side, innganger og seo description-tag",
type: "text",
group: "innhold",
rows: 3,
validation: (Rule) =>
Rule.required().max(210).error("Ingress kan ikke være på over 210 tegn"),
};
export const innholdFieldNew = {
title: "Innhold",
name: "content",
type: "riktekst_aksel",
group: "innhold",
};
export const innholdFieldPrinsipp = {
title: "Innhold",
name: "content",
type: "riktekst_prinsipp",
group: "innhold",
};
export const relevanteArtiklerField = {
title: "Relevante artikler",
description:
"Legg til relaterte artikler som du tenker er relevant å lese (maks 3)",
name: "relevante_artikler",
type: "array",
group: "innhold",
validation: (Rule) =>
Rule.max(3).error("Kan ikke ha mer enn 3 relevante artikler lagt til"),
of: [
{
type: "reference",
weak: true,
to: [...allDocumentTypes.map((x) => ({ type: x }))],
},
],
};
export const innholdFieldNewNested = (name?: string, type?: string) => {
const fields = {
...innholdFieldNew,
type: type ?? "riktekst_ds_artikkel",
name: name ?? "content",
};
delete fields.group;
return fields;
};
export const hidePageFeedback = {
title: "Tilbakemeldinger",
name: "metadata_feedback",
type: "object",
group: "settings",
fields: [
{
title: "Skjul artikkel feedback modul",
description: "Gjemmer <<Var denne artikkelen til hjelp?>> modulen.",
name: "hide_feedback",
type: "boolean",
initialValue: false,
},
],
};
+116
View File
@@ -0,0 +1,116 @@
export * from "./fields";
export * from "./preview";
export * from "./seo";
import { Settings } from "@navikt/ds-icons";
import sanityClient from "part:@sanity/base/client";
const client = sanityClient.withConfig({ apiVersion: "2020-06-19" });
export const prinsipper = [
{ title: "Brukeropplevelse", value: "brukeropplevelse" },
];
export function toPlainText(blocks = []) {
return blocks
.filter((block) => !(block._type !== "block" || !block.children))
.map((block) => {
return block.children.map((child) => child.text).join("");
})
.join("\n");
}
export const sanitySlug = (prefix: string, depth: number, source?: string) => ({
title: "url",
name: "slug",
type: "slug",
validation: (Rule) => validateSlug(Rule, prefix, depth ?? 3),
group: "settings",
options: {
isUnique: isSlugUnique,
source: source ?? "heading",
slugify: (input) =>
`${prefix}${input}`
.toLowerCase()
.trim()
.slice(0, 80)
.trim()
.replace(/\s+/g, "-")
.replace("--", "-")
.replace("(", "")
.replace(")", "")
.replace("æ", "a")
.replace("ø", "o")
.replace("å", "a")
.replace(".", "-"),
},
});
/* Checks that slug starts with prefix and is nested to x depth */
export const validateSlug = (Rule, prefix, nesting) =>
Rule.required().custom((slug) => {
if (!slug.current.startsWith(prefix)) {
return `Slug må starte med prefiks: ${prefix}`;
}
if ((slug.current.match(/\//g) || []).length > nesting - 1) {
return `Siden kan bare være på ${nesting} nivå`;
}
return true;
});
/* Checks every document for matching slug */
export const isSlugUnique = (slug, options) => {
const { document } = options;
const id = document._id.replace(/^drafts\./, "");
const params = {
draft: `drafts.${id}`,
published: id,
slug,
};
const query = `!defined(*[!(_id in [$draft, $published]) && slug.current == $slug][0]._id)`;
return client.fetch(query, params);
};
export const isEditorUnique = (slug, options) => {
const { document } = options;
const id = document._id.replace(/^drafts\./, "");
const params = {
draft: `drafts.${id}`,
published: id,
slug,
};
const query = `!defined(*[!(_id in [$draft, $published]) && _type == "editor" && user_id.current == $slug][0]._id)`;
return client.fetch(query, params);
};
export const groups = [
{
name: "innhold",
title: "Innhold",
default: true,
},
{
name: "metadata",
title: "Metadata",
},
{
name: "settings",
title: "Innstillinger",
icon: Settings,
},
{
name: "lenker",
title: "Linking",
},
{
name: "beta",
title: "Beta",
},
{
name: "seo",
title: "SEO",
},
];
+38
View File
@@ -0,0 +1,38 @@
import { Edit, Task } from "@navikt/ds-icons";
import React from "react";
export const defaultDocPreview = {
preview: {
select: {
heading: "heading",
status: "status",
metadata: "metadata",
id: "_id",
type: "_type",
tema: "tema.0.title",
prinsipp: "prinsipp",
},
prepare(selection) {
const { id, heading, type, tema, prinsipp } = selection;
return {
title: heading,
subtitle: !!prinsipp
? !!prinsipp?.hovedside
? "Hovedside"
: "Artikkel"
: !!tema
? `${tema}`
: type.includes("_component_")
? "Komponentside"
: type.includes("_tabbed_")
? "Artikkel m/tabs"
: "Artikkel",
media: id.includes("drafts.") ? (
<Edit aria-hidden />
) : (
<Task aria-hidden />
),
};
},
},
};
+48
View File
@@ -0,0 +1,48 @@
const pattern = /^image-([a-f\d]+)-(\d+x\d+)-(\w+)$/;
const decodeAssetId = (id) => {
const [, assetId, dimensions, format] = pattern.exec(id);
const [width, height] = dimensions.split("x").map((v) => parseInt(v, 10));
return {
assetId,
dimensions: { width, height },
format,
};
};
export const SEOFields = {
name: "seo",
type: "object",
title: "SEO",
group: "seo",
fields: [
{
name: "meta",
type: "text",
title: "Meta/:og description",
description:
"Anbefalt maks 150-160 bokstaver. Erstatter ingress som <meta /> description",
rows: 3,
},
{
title: "og:Image",
name: "image",
type: "image",
description: "Anbefalt størrelse er 1200:630px",
options: {
accept: "image/*",
},
validation: (Rule) =>
Rule.custom((image) => {
if (!image) return true;
const { dimensions } = decodeAssetId(image.asset._ref);
const aspectR = dimensions.width / dimensions.height;
if (aspectR < 1.5 || aspectR > 2.4) {
return `Forventet aspect-ration rundt 1.91:1. Er nå: ${aspectR}:1.`;
}
return true;
}),
},
],
};
+58
View File
@@ -0,0 +1,58 @@
{
"name": "sanity",
"private": true,
"version": "0.1.0",
"description": "Sanity løsning for Aksel",
"license": "MIT",
"scripts": {
"start": "sanity start",
"build": "sanity build",
"deploy": "yarn typegen && sanity deploy",
"backup": "node scripts/backup.js",
"typegen": "sanity exec typegen.ts"
},
"main": "package.json",
"keywords": [
"sanity"
],
"dependencies": {
"@navikt/ds-css": "^1.4.0",
"@navikt/ds-css-internal": "^1.4.0",
"@navikt/ds-icons": "^1.4.0",
"@navikt/ds-react": "^1.4.0",
"@navikt/ds-react-internal": "^1.4.0",
"@sanity/base": "^2.35.0",
"@sanity/cli": "^2.35.0",
"@sanity/code-input": "^2.35.0",
"@sanity/components": "^2.14.0",
"@sanity/core": "^2.35.0",
"@sanity/dashboard": "^2.35.0",
"@sanity/default-layout": "^2.35.0",
"@sanity/default-login": "^2.35.0",
"@sanity/desk-tool": "^2.35.0",
"@sanity/export": "^2.30.3",
"@sanity/production-preview": "^2.29.3",
"@sanity/react-hooks": "^2.35.0",
"@sanity/vision": "^2.35.0",
"moment": "2.29.4",
"sanity-codegen": "1.0.0-alpha.26",
"sanity-plugin-asset-source-unsplash": "^0.1.5",
"sanity-plugin-dashboard-widget-document-list": "^0.0.13",
"sanity-plugin-media": "^1.4.4",
"sanity-plugin-power-table": "^0.0.14",
"sanity-super-pane": "^1.0.0-alpha.11",
"semver": "^7.3.5",
"styled-components": "^5.3.1"
},
"devDependencies": {
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.0.0",
"@sanity-codegen/schema-codegen": "1.0.0-alpha.13",
"babel-plugin-mock-imports": "1.2.0"
},
"repository": {
"type": "git",
"url": "https://github.com/navikt/aksel-website.git"
}
}
+1
View File
@@ -0,0 +1 @@
User-specific packages can be placed here
@@ -0,0 +1,137 @@
/* https://github.com/sanity-io/dashboard-widget-document-list/blob/master/src/DocumentList.js */
import client from "@sanity/client";
import { Button, Flex } from "@sanity/ui";
import { DashboardWidget } from "@sanity/dashboard";
import React, { useEffect, useState, useRef, useCallback } from "react";
import config from "../../config";
import SanityConfig from "../../sanity.json";
import moment from "moment";
import styles from "./widget.css";
import { Item, List } from "part:@sanity/components/lists/default";
import { IntentLink } from "part:@sanity/base/router";
import { getPublishedId } from "part:@sanity/base/util/draft-utils";
import { Tag } from "@navikt/ds-react";
const sanityClient = client({
projectId: SanityConfig.api.projectId,
dataset: SanityConfig.api.dataset,
apiVersion: "2020-06-19",
useCdn: false,
});
const OutDatedComponents = ({ type }) => {
const query = `*[_type in [${config.allDocumentTypes.map((x) => `"${x}"`)}]]`;
const [docs, setDocs] = useState([]);
const [loading, setloading] = useState(true);
const [timer, setTimer] = useState(new Date());
const [fromTime, setFromTime] = useState(moment(new Date()).fromNow());
const success = (v) => {
setDocs(
v
.filter((x) => {
if (!x?._updatedAt) return false;
const lastUpdate = moment(x._updatedAt);
const daysSince = moment().diff(lastUpdate, "days");
return type === "error"
? daysSince > 180
: daysSince >= 90 && daysSince < 180;
})
.sort((a, b) => {
const lastUpdateA = moment(a._updatedAt);
const lastUpdateB = moment(b._updatedAt);
return lastUpdateA.diff(lastUpdateB, "days");
})
);
setloading(false);
setTimer(new Date());
};
const load = useCallback(() => {
setloading(true);
sanityClient.fetch(query).then(success, (e) => console.error(e));
}, []);
useEffect(() => {
load();
}, []);
/* Refreshes every 5 minutes */
useEffect(() => {
const interval = setInterval(() => {
load();
}, 300000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const interval = setInterval(() => {
setFromTime(moment(timer).fromNow());
}, 10000);
return () => clearInterval(interval);
}, []);
const refresh = () => {
load();
};
const footer = (
<Flex direction="column" align="stretch">
<Button
fontSize={[2]}
tone="primary"
paddingX={2}
paddingY={4}
text={`Updated ${fromTime}`}
onClick={() => refresh()}
mode="bleed"
loading={loading}
disabled={loading}
/>
</Flex>
);
return (
<DashboardWidget
header={type === "error" ? "Utdaterte sider" : "Stagnerte sider"}
footer={footer}
>
<List>
{docs &&
docs.map((doc) => {
const lastUpdate = moment(doc._updatedAt);
const daysSince = moment().diff(lastUpdate, "days");
return (
<Item key={doc._id}>
<IntentLink
intent="edit"
params={{
type: doc._type,
id: getPublishedId(doc._id),
}}
className={styles.link}
>
<span className={styles.spacing}>
<Tag size="small" variant={type}>
{daysSince}d
</Tag>
{doc?.heading}
</span>
</IntentLink>
</Item>
);
})}
</List>
</DashboardWidget>
);
};
export default {
name: "outdated-documents-widget",
component: OutDatedComponents,
};
@@ -0,0 +1,9 @@
{
"parts": [
{
"name": "part:@sanity/dashboard/widget/outdated-documents-widget",
"implements": "part:@sanity/dashboard/widget",
"path": "index.js"
}
]
}
@@ -0,0 +1,47 @@
@import "part:@sanity/base/theme/variables-style";
.container {
composes: container from "part:@sanity/dashboard/widget-styles";
}
.header {
composes: header from "part:@sanity/dashboard/widget-styles";
}
.title {
composes: title from "part:@sanity/dashboard/widget-styles";
}
.content {
composes: content from "part:@sanity/dashboard/widget-styles";
border-top: 1px solid var(--hairline-color);
padding: var(--small-padding) 0;
display: flex;
justify-content: center;
}
.link {
composes: item from "part:@sanity/base/theme/layout/selectable-style";
display: block;
color: inherit;
text-decoration: inherit;
outline: none;
padding: var(--small-padding) var(--medium-padding);
height: 100%;
}
.footer {
composes: footer from "part:@sanity/dashboard/widget-styles";
border-top: 1px solid var(--hairline-color);
}
.img {
display: block;
width: 100%;
}
.spacing {
display: inline-flex;
gap: 1rem;
align-items: center;
}
+70
View File
@@ -0,0 +1,70 @@
{
"root": true,
"project": {
"name": "Aksel"
},
"api": {
"projectId": "hnbe3yhs",
"dataset": "production"
},
"env": {
"development": {
"api": {
"dataset": "production"
},
"plugins": ["@sanity/vision"]
}
},
"plugins": [
"@sanity/base",
"@sanity/components",
"@sanity/default-layout",
"@sanity/default-login",
"@sanity/desk-tool",
"@sanity/code-input",
"@sanity/dashboard",
"dashboard-widget-document-list",
"outdated-documents-widget",
"media",
"power-table",
"asset-source-unsplash"
],
"parts": [
{
"name": "part:@sanity/base/schema",
"path": "./schemas/schema"
},
{
"name": "part:@sanity/desk-tool/structure",
"path": "./client/desk-structure/deskStructure.tsx"
},
{
"implements": "part:@sanity/dashboard/config",
"path": "./client/dashboardConfig.tsx"
},
{
"name": "part:@sanity/base/initial-value-templates",
"path": "./client/initialValueTemplates.tsx"
},
{
"name": "part:@sanity/base/new-document-structure",
"path": "./client/newDocumentStructure.tsx"
},
{
"implements": "part:@sanity/base/absolutes",
"path": "./client/bundleCheck.tsx"
},
{
"implements": "part:@sanity/form-builder/input/image/asset-sources",
"path": "./client/assetSources.ts"
},
{
"implements": "part:@sanity/form-builder/input/file/asset-sources",
"path": "./client/assetSources.ts"
},
{
"implements": "part:@sanity/base/document-actions/resolver",
"path": "./client/resolveDocumentActions.tsx"
}
]
}
+1
View File
@@ -0,0 +1 @@
export default null;
@@ -0,0 +1,44 @@
import {
defaultDocPreview,
editorField,
groups,
ingressField,
innholdFieldNew,
migratedField,
publishedAtField,
relevanteArtiklerField,
sanitySlug,
SEOFields,
titleField,
} from "@/lib";
const prefix = "artikkel/";
export default {
title: "Aksel Artikkel",
name: "aksel_artikkel",
type: "document",
groups,
...defaultDocPreview,
fields: [
publishedAtField,
editorField,
titleField,
sanitySlug(prefix, 2),
{
title: "Tema",
description: "Legg til de viktigeste temaene",
name: "tema",
type: "array",
of: [{ type: "reference", to: [{ type: "aksel_tema" }] }],
group: "innhold",
},
ingressField,
innholdFieldNew,
relevanteArtiklerField,
SEOFields,
migratedField,
],
};
@@ -0,0 +1,33 @@
import {
defaultDocPreview,
editorField,
groups,
ingressField,
innholdFieldNew,
migratedField,
publishedAtField,
sanitySlug,
SEOFields,
titleField,
} from "@/lib";
const prefix = "blogg/";
export default {
title: "Blogg",
name: "aksel_blogg",
type: "document",
groups,
...defaultDocPreview,
fields: [
publishedAtField,
editorField,
titleField,
sanitySlug(prefix, 2),
ingressField,
innholdFieldNew,
SEOFields,
migratedField,
],
};
@@ -0,0 +1,143 @@
import {
defaultDocPreview,
editorField,
groups,
ingressField,
innholdFieldPrinsipp,
isSlugUnique,
migratedField,
prinsipper,
publishedAtField,
SEOFields,
titleField,
} from "@/lib";
import sanityClient from "part:@sanity/base/client";
const client = sanityClient.withConfig({ apiVersion: "2020-06-19" });
const prefix = "prinsipper/";
export default {
title: "Aksel Prinsipp",
name: "aksel_prinsipp",
type: "document",
groups,
...defaultDocPreview,
fields: [
publishedAtField,
editorField,
titleField,
{
title: "url",
name: "slug",
type: "slug",
validation: (Rule) =>
Rule.required().custom((slug, { document }) => {
if (!slug || !slug.current)
return `Må ha noe innhold. Har du husket å velget et prinsipp først?`;
const maxLength = document?.prinsipp?.hovedside ? 2 : 3;
const newPrefix = `${prefix}${document?.prinsipp?.prinsippvalg}`;
if (!document?.prinsipp?.prinsippvalg)
return `Siden må ha valgt prinsipp for å sette opp url`;
if (document?.prinsipp?.hovedside && slug.current !== newPrefix) {
return `Siden er en hovedside for prinsippet og må derfor være på url ${newPrefix}`;
}
if (
!document?.prinsipp?.hovedside &&
!slug.current.startsWith(newPrefix)
) {
return `Slug må starte med: ${newPrefix}`;
}
if (
(slug.current.split("/") || []).filter((x) => !!x).length !==
maxLength
) {
return `Siden må være på ${maxLength} nivå`;
}
return true;
}),
group: "settings",
options: {
isUnique: isSlugUnique,
source: ({ prinsipp = null, heading = null }) => ({
prinsipp,
heading,
}),
slugify: ({ prinsipp, heading }) => {
if (!prinsipp || !heading) return "";
const rest = prinsipp.hovedside ? "" : `/${heading}`;
return `${prefix}${prinsipp.prinsippvalg}${rest}`
.toLowerCase()
.trim()
.slice(0, 80)
.trim()
.replace(/\s+/g, "-")
.replace("æ", "a")
.replace("ø", "o")
.replace("å", "a");
},
},
},
{
title: "Prinsipp",
description: "Velg prinsippet siden omhandler",
name: "prinsipp",
group: "innhold",
type: "object",
fields: [
{
title: "Velg prinsipp",
name: "prinsippvalg",
type: "string",
options: {
list: prinsipper,
layout: "radio",
},
validation: (Rule) =>
Rule.required().error("Prinsippsider må ha valgt et prinsipp"),
},
{
title: "Er denne siden hovedsiden til Prinsippet?",
name: "hovedside",
type: "boolean",
initialValue: false,
validation: (Rule) =>
Rule.required().custom(async (_, { document, parent }) => {
const id = document._id.replace(/^drafts\./, "");
const params = {
draft: `drafts.${id}`,
published: id,
hoved: parent?.hovedside,
prinsipp: parent?.prinsippvalg,
};
const query = `*[!(_id in [$draft, $published]) && _type == "aksel_prinsipp" && prinsipp.prinsippvalg == $prinsipp && prinsipp.hovedside == true][0]{heading,_id}`;
const res = await client.fetch(query, params);
if (parent?.hovedside && !!res?._id) {
return `Kan bare ha en hovedside for hvert prinsipp. Hovedsiden er nå: ${
res?.heading ?? "Siden har ikke en heading"
}`;
} else if (!parent?.hovedside && !res?._id) {
return `Hvert prinsipp må ha minst en hovedside. Velg en hovedside før du fortsetter`;
}
return true;
}),
},
],
},
{
title: "Hero bilde",
name: "hero_bilde",
type: "herobilde",
group: "innhold",
hidden: ({ parent }) => !parent.prinsipp.hovedside,
},
ingressField,
innholdFieldPrinsipp,
SEOFields,
migratedField,
],
};
@@ -0,0 +1,26 @@
import {
defaultDocPreview,
groups,
innholdFieldNew,
migratedField,
publishedAtField,
sanitySlug,
titleField,
} from "@/lib";
const prefix = "side/";
export default {
title: "Standalone-sider",
name: "aksel_standalone",
type: "document",
groups,
...defaultDocPreview,
fields: [
publishedAtField,
titleField,
sanitySlug(prefix, 2),
innholdFieldNew,
migratedField,
],
};
@@ -0,0 +1,5 @@
export { default as Artikkel } from "./articles/artikkel";
export { default as Blogg } from "./articles/blogg";
export { default as Tema } from "./tema";
export { default as Prinsipp } from "./articles/prinsipp";
export { default as Standalone } from "./articles/standalone";
@@ -0,0 +1,111 @@
import { groups } from "@/lib";
export default {
title: "Aksel Tema",
name: "aksel_tema",
type: "document",
groups,
fields: [
{
title: "Navn",
name: "title",
type: "string",
group: "innhold",
validation: (Rule) => Rule.required().error("Temaet må ha et navn"),
},
{
title: "Kort Intro/Oppsummering",
description: "Brukes i kort og innganger",
name: "oppsummering",
type: "string",
group: "innhold",
validation: (Rule) =>
Rule.required()
.max(65)
.error("Temaet burde ha en kort oppsummering/intro på max 65tegn"),
},
{
title: "Beskrivelse",
name: "beskrivelse",
type: "riktekst_enkel",
group: "innhold",
},
{
title: "Ansvarlig for tema",
description: "Legg til redaktør som har forvaltningsansvaret for temaet",
name: "ansvarlig",
type: "reference",
to: [{ type: "editor" }],
validation: (Rule) => Rule.required(),
group: "innhold",
},
{
title: "Bruk seksjonsinndeling",
description:
"Vil ikke vise artikler som ikke er lagt til i seksjoner hvis valgt!",
name: "bruk_seksjoner",
type: "boolean",
group: "innhold",
initialValue: false,
},
{
title: "Seksjonering",
description:
"Del inn artiklene i flere seksjoner (vises ikke i preview før publisering desverre)",
name: "seksjoner",
type: "array",
group: "innhold",
hidden: ({ parent }) => !parent?.bruk_seksjoner,
of: [
{
type: "object",
title: "Seksjon",
name: "seksjon",
fields: [
{
type: "string",
name: "title",
title: "Tittel",
validation: (Rule) =>
Rule.required().error("Seksjonen må ha et navn"),
},
{
title: "Beskrivelse",
name: "beskrivelse",
type: "riktekst_enkel",
},
{
name: "sider",
type: "array",
title: "Sider",
of: [
{
type: "reference",
name: "ref",
title: "Side",
validation: (Rule) =>
Rule.required().error("Seksjonen må ha minst en referanse"),
to: [{ type: "aksel_artikkel" }],
options: {
filter: ({ document, parent }) => {
const selected = parent
.filter((x) => !!x._ref)
.map((x) => x._ref);
return {
filter: "references($refId) && !(_id in $docIds)",
params: {
refId: document._id.replace("drafts.", ""),
docIds: selected,
},
};
},
},
},
],
},
],
},
],
},
],
};
@@ -0,0 +1,83 @@
import {
defaultDocPreview,
editorField,
groups,
hidePageFeedback,
innholdFieldNew,
innholdFieldNewNested,
migratedField,
publishedAtField,
sanitySlug,
titleField,
} from "@/lib";
const prefix = "designsystem/side/";
export default {
title: "Artikkel",
name: "ds_artikkel",
type: "document",
groups,
...defaultDocPreview,
fields: [
migratedField,
publishedAtField,
editorField,
titleField,
sanitySlug(prefix, 3),
{
title: "Layout",
description:
"'Full' bruker hele bredden, men fjerner table of content (bruk bare for eks ikonside eller komponentvisning)",
name: "layout",
type: "string",
group: "settings",
options: {
list: [
{ title: "Full", value: "full" },
{ title: "Default", value: "default" },
],
layout: "radio",
},
initialValue: "default",
},
{
type: "boolean",
name: "artikkel_type",
title: "Bruk Tabs",
group: "innhold",
initialValue: false,
},
{
...innholdFieldNew,
type: "riktekst_ds_artikkel",
hidden: ({ document }) => !!document?.artikkel_type,
},
{
name: "content_tabs",
title: "Innhold i Tabs",
type: "array",
group: "innhold",
hidden: ({ document }) => !document?.artikkel_type,
of: [
{
name: "tab",
title: "Tab",
type: "object",
fields: [
{
title: "Tittel",
description: "Innhold vil da legges under url/tab-tittel",
name: "title",
type: "string",
validation: (Rule) =>
Rule.required().error("Tabben må ha en enkel tittel"),
},
innholdFieldNewNested(),
],
},
],
},
hidePageFeedback,
],
};
@@ -0,0 +1,93 @@
import {
defaultDocPreview,
editorField,
groups,
innholdFieldNew,
migratedField,
publishedAtField,
sanitySlug,
titleField,
} from "@/lib";
const prefix = "designsystem/komponenter/";
export default {
title: "Komponentartikkel",
name: "komponent_artikkel",
type: "document",
groups,
...defaultDocPreview,
fields: [
publishedAtField,
editorField,
titleField,
sanitySlug(prefix, 3),
{
title: "Metadata",
name: "status",
group: "innhold",
type: "object",
fields: [
{
title: "Status",
name: "tag",
type: "string",
initialValue: "new",
options: {
list: [
{ title: "Beta", value: "beta" },
{ title: "New", value: "new" },
{ title: "Ready", value: "ready" },
{ title: "Deprecated", value: "deprecated" },
],
layout: "radio",
},
validation: (Rule) => Rule.required(),
},
{
name: "bilde",
title: "Thumbnail/og-bilde",
type: "image",
},
],
options: {
collapsible: true,
collapsed: true,
},
},
{
name: "intro",
type: "intro_komponent",
group: "innhold",
},
{
...innholdFieldNew,
type: "riktekst_komponent",
name: "bruk_tab",
title: "Innhold",
},
{
title: "Kodepakker",
name: "kodepakker",
type: "array",
of: [{ type: "string" }],
options: {
list: [
{ title: "ds-react", value: "ds-react" },
{ title: "ds-css", value: "ds-css" },
{ title: "ds-react-internal", value: "ds-react-internal" },
{ title: "ds-css-internal", value: "ds-css-internal" },
{ title: "ds-icons", value: "ds-icons" },
{ title: "ds-tokens", value: "ds-tokens" },
{ title: "ds-tailwind", value: "ds-tailwind" },
],
},
},
{
title: "Figma lenke (optional)",
name: "figma_link",
type: "url",
group: "lenker",
},
],
};
@@ -0,0 +1,93 @@
export default {
title: "Fargekategori",
name: "ds_color_categories",
type: "document",
fields: [
{
title: "Tittel",
name: "title",
type: "string",
validation: (Rule) => Rule.required(),
},
{
title: "Beskrivelse",
name: "description",
type: "riktekst_enkel",
},
{
type: "array",
name: "colors",
title: "Farger",
of: [{ type: "ds_color" }],
options: {
sortable: false,
},
},
],
};
export const Color = {
name: "ds_color",
title: "Farge",
type: "object",
fields: [
{
name: "title",
title: "Navn",
type: "string",
validation: (Rule) => Rule.required(),
readOnly: true,
},
{
name: "full_title",
title: "CSS variabelnavn",
type: "string",
validation: (Rule) => Rule.required(),
readOnly: true,
},
{
title: "Fargetype/nivå",
name: "color_type",
type: "string",
validation: (Rule) => Rule.required(),
initialValue: "global",
readOnly: true,
options: {
layout: "radio",
list: [
{ value: "global", title: "Global" },
{ value: "semantic", title: "Semantic" },
],
},
},
{
name: "color_name",
title: "Brukt fargenavn",
type: "string",
readOnly: true,
hidden: ({ parent }) => parent?.color_type === "global",
},
{
name: "color_roles",
title: "Fargeroller",
type: "array",
hidden: ({ parent }) => parent?.color_type === "global",
of: [{ type: "string", name: "role" }],
},
{
name: "color_value",
title: "Brukt farge",
type: "string",
readOnly: true,
validation: (Rule) => Rule.required(),
},
{
name: "color_index",
title: "Fargeindeks",
type: "number",
description:
"Brukes for å endre rekkefølgen på listen. Farger med indeks 0 vil være først, større tall sorteres under der igjen",
},
],
__experimental_omnisearch_visibility: false,
};
@@ -0,0 +1,57 @@
/* import { ExampleKeys } from "website/component-examples"; */
export default {
title: "Dir/filnavn for kode eksempler",
name: "kode_eksempler_fil",
type: "document",
fields: [
{
title: "Tittel",
name: "title",
type: "string",
validation: (Rule) => Rule.required(),
readOnly: true,
},
{
title: "Toggle om eksemplet er et dir eller filnavn",
name: "dir",
type: "boolean",
validation: (Rule) => Rule.required(),
initialValue: false,
readOnly: true,
},
{
title: "Filer",
name: "filer",
type: "array",
readOnly: true,
of: [
{
title: "Data i filer",
name: "fil",
type: "object",
fields: [
{ title: "Filnavn", name: "navn", type: "string" },
{ title: "Innhold", name: "innhold", type: "string" },
{ title: "Beskrivelse", name: "description", type: "text" },
],
},
],
},
],
preview: {
select: {
title: "title",
dir: "dir",
},
prepare(selection) {
const { title, dir } = selection;
return {
title: title,
subtitle: dir ? "Alle kode-eksempler" : "Spesifikt kode-eksempel",
media: () => "</>",
};
},
},
__experimental_omnisearch_visibility: false,
};
@@ -0,0 +1,88 @@
/* import { ExampleKeys } from "website/component-examples"; */
export default {
title: "Autogenerert Propdata",
name: "ds_props",
type: "document",
fields: [
{
title: "Tittel",
name: "title",
type: "string",
readOnly: true,
},
{
title: "Displayname",
name: "displayname",
type: "string",
readOnly: true,
},
{
title: "Filepath",
name: "filepath",
type: "string",
readOnly: true,
},
{
title: "props",
name: "proplist",
type: "array",
readOnly: true,
of: [
{
title: "Prop",
name: "prop",
type: "object",
fields: [
{
title: "Name",
name: "name",
type: "string",
},
{
title: "DefaultValue",
name: "defaultValue",
type: "string",
},
{
title: "Description",
name: "description",
type: "string",
},
{
title: "Required",
name: "required",
type: "boolean",
},
{
title: "Type",
name: "type",
type: "string",
},
{
title: "isRef",
name: "ref",
type: "boolean",
},
],
},
],
},
],
preview: {
select: {
title: "title",
filepath: "filepath",
id: "_id",
},
prepare(selection) {
const { title, filepath, id } = selection;
const str = id.includes("core") ? "ds-react" : "ds-internal";
return {
title,
subtitle: `${str}: ${filepath}`,
};
},
},
__experimental_omnisearch_visibility: false,
};
@@ -0,0 +1,45 @@
export default {
title: "Tokens",
name: "ds_tokens",
type: "document",
fields: [
{
title: "Navn",
name: "title",
type: "string",
validation: (Rule) => Rule.required(),
readOnly: true,
},
{
title: "Verdi",
name: "token",
type: "string",
validation: (Rule) => Rule.required(),
readOnly: true,
},
{
title: "Faktisk verdi",
name: "raw",
type: "string",
readOnly: true,
},
{
title: "Parent",
name: "parent",
type: "string",
readOnly: true,
},
{
title: "Beskrivelse",
name: "beskrivelse",
type: "string",
},
{
title: "Autogenerert",
name: "autogenerated",
type: "boolean",
readOnly: true,
},
],
__experimental_omnisearch_visibility: false,
};
@@ -0,0 +1,7 @@
import { KomponentArtikkel } from "./index";
export default {
...KomponentArtikkel,
title: "Komponentartikkel-template",
name: "ds_component_template",
};
@@ -0,0 +1,77 @@
export default {
title: "Forside",
name: "ds_frontpage",
type: "document",
fields: [
{
name: "body",
type: "riktekst_enkel",
title: "Innhold",
validation: (Rule) => Rule.required(),
},
{
name: "cards",
type: "array",
title: "Cards",
of: [
{
title: "Card",
name: "card",
type: "object",
fields: [
{
title: "Lenke",
name: "link_ref",
type: "reference",
to: [{ type: "komponent_artikkel" }, { type: "ds_artikkel" }],
validation: (Rule) => Rule.required(),
},
{
title: "Tittel",
name: "title",
type: "string",
},
{
title: "Innhold",
name: "content",
type: "string",
},
{
title: "Pictogram",
name: "picture",
type: "image",
options: {
hotspot: true,
},
fields: [
{
name: "title",
title: "Alt-tekst",
type: "string",
validation: (Rule) =>
Rule.required().error("Bilde må ha en alt-tekst"),
description: "Beskriv bildet for skjermlesere",
options: {
isHighlighted: true,
},
},
],
validation: (Rule) =>
Rule.required().error("Må legge til et pictogram"),
},
],
options: {
modal: {
type: "dialog",
width: "medium", // 'small' | 'medium' | 'large' | 'full'
},
},
},
],
validation: (Rule) => Rule.required(),
},
],
preview: {
prepare: () => ({ title: "Forside designsystemet" }),
},
};
@@ -0,0 +1,13 @@
export { default as KomponentArtikkel } from "./articles/komponent";
export { default as Artikkel } from "./articles/artikkel";
export { default as ComponentTemplate } from "./component-template";
export { default as Props } from "./automated/props";
export { default as Tokens } from "./automated/tokens";
export { default as KodeEksempler } from "./automated/kode-eksempler";
export {
Color,
default as ColorCategories,
} from "./automated/color-categories";
export { default as Frontpage } from "./frontpage";
export { default as Navigation, ds_header_heading } from "./navigation";
@@ -0,0 +1,129 @@
export default {
name: "ds_navigation",
title: "Navigation",
type: "document",
fields: [
{
title: "Designsystem navigajsons-struktur",
name: "title",
type: "string",
readOnly: true,
hidden: true,
initialValue: "Designsystem navigajsons-struktur",
},
{
name: "headings",
title: "Header linker",
type: "array",
of: [
{
type: "ds_navigation_heading",
name: "heading",
title: "Header link",
},
],
validation: (Rule) =>
Rule.required()
.min(1)
.error("Headingmenyen må ha minst en koblet lenke i sidemenyen"),
},
],
};
export const ds_header_heading = {
name: "ds_navigation_heading",
title: "Header link",
type: "object",
fields: [
{
name: "title",
title: "Heading tittel",
type: "string",
validation: (Rule) =>
Rule.required().error("Header lenken må ha en tittel"),
},
{
title: "Side selve headingen linker til",
description:
"Husk å legge denne til i menyen også, hvis ikke blir den bare tilgjengelig via headern",
name: "link_ref",
type: "reference",
to: [{ type: "komponent_artikkel" }, { type: "ds_artikkel" }],
validation: (Rule) =>
Rule.required().error("Header lenken må linke til en startside"),
},
{
title: "Meny for denne headingen",
name: "menu",
type: "array",
validation: (Rule) =>
Rule.required().error("Sidemeny må ha misnt en lenke"),
of: [
{
title: "Menypunkt",
name: "item",
type: "object",
fields: [
{
title: "Menypunkt tittel",
name: "title",
type: "string",
validation: (Rule) =>
Rule.required().error("Sidemeny-lenken må ha en tittel"),
},
{
title: "Link til side",
name: "link",
type: "reference",
to: [{ type: "komponent_artikkel" }, { type: "ds_artikkel" }],
options: {
modal: {
type: "dialog",
width: "medium", // 'small' | 'medium' | 'large' | 'full'
},
},
validation: (Rule) =>
Rule.required().error("Sidemeny-lenken må lenke til en side"),
},
],
},
{
title: "Subheading",
name: "subheading",
type: "object",
fields: [
{
title: "Subheading",
name: "title",
type: "string",
validation: (Rule) =>
Rule.required().error("Subheading må være fylt ut"),
},
],
preview: {
select: {
title: "title",
},
prepare(selection) {
const { title } = selection;
return {
title: title,
subtitle: "Subheading",
};
},
},
},
],
},
],
preview: {
select: {
title: "title",
},
prepare({ title }) {
return {
title: `${title}`,
};
},
},
};
@@ -0,0 +1,68 @@
import { People } from "@navikt/ds-icons";
import React from "react";
import profilePage from "../../components/profile/profile-page";
import { isEditorUnique } from "@/lib";
import userStore from "part:@sanity/base/user";
export default {
title: "Redaktører",
name: "editor",
type: "document",
fields: [
{
title: "Navn",
name: "title",
type: "string",
validation: (Rule) => Rule.required().error("Må legge til navn"),
},
{
title: "Sanity bruker-id",
name: "user_id",
type: "slug",
validation: (Rule) => Rule.required().error("Må ha Id"),
options: {
isUnique: isEditorUnique,
source: async () => {
const { id } = await userStore.getUser("me");
return id;
},
slugify: (input) => input,
},
},
{
title: "Roller",
description: "eks: Utvikler, Webanalytiker, uu-spesialist",
name: "roller",
type: "array",
of: [{ type: "string" }],
options: {
layout: "tags",
},
},
{
name: "profile_page",
type: "string",
title: "Profil",
inputComponent: profilePage,
hidden: ({ currentUser, parent }) => {
const { id, roles } = currentUser;
return (
!roles.find(({ name }) => name === "administrator") &&
parent?.user_id?.current !== id
);
},
},
],
preview: {
select: {
title: "title",
},
prepare(selection) {
const { title } = selection;
return {
title,
media: () => <People />,
};
},
},
};
@@ -0,0 +1,58 @@
import { groups, SEOFields } from "@/lib";
export default {
title: "Forside",
name: "vk_frontpage",
type: "document",
groups,
fields: [
{
title: "Tittel",
name: "title",
type: "string",
validation: (Rule) => Rule.required(),
},
{
title: "Beskrivelse",
name: "beskrivelse",
type: "riktekst_enkel",
validation: (Rule) => Rule.required(),
},
{
title: "Brukeropplevelse",
name: "prinsipp_1",
type: "object",
fields: [
{
title: "Vis på forside",
name: "vis",
type: "boolean",
validation: (Rule) => Rule.required(),
},
{
title: "Beskrivelse",
name: "beskrivelse",
type: "riktekst_enkel",
validation: (Rule) => Rule.required(),
},
{
title: "Hovedside",
name: "hovedside",
type: "reference",
weak: true,
to: [{ type: "aksel_prinsipp" }],
},
{
title: "Undersider",
description: "Rekkefølge bestemmer rekkefølgen på forsiden!",
name: "undersider",
type: "array",
of: [
{ type: "reference", weak: true, to: [{ type: "aksel_prinsipp" }] },
],
},
],
},
SEOFields,
],
};
@@ -0,0 +1,153 @@
import { Expand, Link } from "@navikt/ds-icons";
import React from "react";
const config = require("../../config");
const maxDepth = 2;
const checkDepth = (list, depth) => {
if (!list || list.length === 0) {
throw new Error("Dropdown lister må ha mist et element");
}
if (depth > maxDepth) {
throw new Error(`Sidemeny kan ha maks dybde på ${maxDepth} elementer`);
}
for (const el of list) {
el._type === "dropdown" && checkDepth(el.dropdown, depth + 1);
}
return true;
};
function validateNestedDepth(sidemenu) {
if (!sidemenu) {
return "Sidemeny må ha minst et element";
}
try {
checkDepth(sidemenu, 0);
} catch (e) {
return e.message;
}
return true;
}
export default {
name: "navigation",
title: "Navigation",
type: "document",
fields: [
{
name: "title",
title: "Tittel",
type: "string",
readOnly: true,
hidden: true,
},
{
name: "sidemenu",
title: "Sidemeny",
description:
"Linker eller dropdowns med linker. Maks dybde på 2 dropdowns er støttet. Sider må være publisert før de kan linkes her.",
type: "array",
of: [
{ type: "navigation_dropdown", name: "dropdown", title: "Dropdown" },
{ type: "navigation_link", name: "link", title: "Link" },
],
validation: (Rule) => Rule.required().min(1),
},
],
validation: (Rule) =>
Rule.required().custom(({ sidemenu, ...rest }) => {
return validateNestedDepth(sidemenu);
}),
};
export const dropdown = {
name: "navigation_dropdown",
title: "Dropdown",
type: "object",
fields: [
{
name: "title",
title: "Tittel",
type: "string",
validation: (Rule) => Rule.required(),
},
{
title: "Meny",
name: "dropdown",
type: "array",
of: [
{ type: "navigation_link", name: "link", title: "Link" },
{ type: "navigation_dropdown", name: "dropdown", title: "Dropdown" },
],
validation: (Rule) => Rule.required().min(1),
},
],
preview: {
select: {
title: "title",
},
prepare({ title }) {
return {
title: `${title}`,
media: <Expand />,
};
},
},
};
export const link = {
name: "navigation_link",
title: "Link",
type: "object",
fields: [
{
name: "title",
title: "Tittel",
type: "string",
validation: (Rule) => Rule.required(),
},
{
title: "Link",
name: "link_ref",
type: "reference",
weak: true,
to: [{ type: "komponent_artikkel" }, { type: "ds_artikkel" }],
validation: (Rule) => Rule.required(),
/* Matches results based on document prefix */
options: {
filter: ({ document }) => {
const match = config.teams.find((team) =>
document._id.endsWith(team.name)
);
if (!match) {
return {
filter: ``,
};
}
return {
filter: `_type match ["${match.prefix}_*", "*_page"]`,
};
},
},
},
],
options: {
modal: {
type: "dialog",
width: "medium", // 'small' | 'medium' | 'large' | 'full'
},
},
preview: {
select: {
title: "title",
},
prepare({ title }) {
return {
title: `${title}`,
media: <Link />,
};
},
},
};
@@ -0,0 +1,75 @@
import { Expand } from "@navikt/ds-icons";
import { innholdFieldNewNested } from "@/lib";
import React from "react";
import { BodyLong, Label } from "@navikt/ds-react";
export default {
name: "accordion",
title: "Accordion",
type: "object",
fields: [
{ title: "Tittel (optional)", type: "string", name: "title" },
{
title: "Liste",
description: "Legg til en eller flere elementer",
name: "list",
type: "array",
of: [
{
title: "Accordion",
name: "element",
type: "object",
fields: [
{
title: "Tittel ",
name: "title",
type: "string",
validation: (Rule) =>
Rule.required().error("Accordion må ha en tittel"),
},
innholdFieldNewNested("content", "riktekst_aksel"),
],
preview: {
select: {
title: "title",
},
prepare(selection) {
return {
title: selection.title ?? "",
media: () => <Expand />,
};
},
},
},
],
validation: (Rule) =>
Rule.required().min(1).error("Accordion modul må ha minst 1 element"),
},
],
preview: {
select: {
title: "title",
list: "list",
},
prepare(s) {
return {
title: s?.title ? s.title : "Accordion",
list: s?.list,
};
},
component: ({ value }) => {
return value?.list ? (
<BodyLong as="div">
<Label as="p">{`Accordion (${value.list.length})`}</Label>
<ul>
{value.list.map((x, y) => (
<li key={x._key}>{x.title}</li>
))}
</ul>
</BodyLong>
) : (
<Label as="p">Accordion</Label>
);
},
},
};
@@ -0,0 +1,68 @@
import { toPlainText } from "@/lib";
import { Warning } from "@navikt/ds-icons";
import React from "react";
export default {
name: "alert",
title: "Alert",
type: "object",
icon: Warning,
fields: [
{
title: "Variant",
name: "variant",
type: "string",
validation: (Rule) => Rule.required().error("Alert må ha en variant"),
options: {
list: [
{ value: "success", title: "Suksess" },
{ value: "info", title: "Info" },
{ value: "warning", title: "Fare" },
{ value: "error", title: "Error" },
],
layout: "radio",
},
initialValue: "info",
},
{
title: "Heading (optional)",
name: "heading",
type: "string",
},
{
title: "Heading nivå",
name: "heading_level",
type: "string",
validation: (Rule) =>
Rule.required().error("Alert må ha et heading-nivå"),
options: {
list: [
{ value: "h3", title: "H3" },
{ value: "h4", title: "H4" },
],
layout: "radio",
},
hidden: ({ parent }) => !parent.heading,
initialValue: "h3",
},
{
title: "Innhold",
name: "body",
type: "riktekst_enkel",
validation: (Rule) => Rule.required().error("Alert må ha noe innhold"),
},
],
preview: {
select: {
variant: "variant",
body: "body",
},
prepare(selection) {
return {
title: toPlainText(selection?.body),
subtitle: `Alert - ${selection.variant}`,
media: () => <Warning />,
};
},
},
};
@@ -0,0 +1,105 @@
import { Picture } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Bilde",
name: "bilde",
type: "image",
icon: Picture,
options: {
hotspot: true,
},
fields: [
{
name: "alt",
title: "Alt-tekst",
type: "string",
validation: (Rule) => Rule.required().error("Bilde må ha en alt-tekst"),
description: "Beskriv bildet for skjermlesere",
options: {
isHighlighted: true,
},
},
{
name: "caption",
title: "Bilde-tekst (optional)",
description: "Dette vil stå under bildet",
type: "string",
hidden: ({ parent }) => parent?.floating,
options: {
isHighlighted: true,
},
},
{
name: "small",
title: "Bildet tar bare ~halve bredden",
type: "boolean",
initialValue: false,
hidden: ({ parent }) => parent?.floating || parent?.hide_floating,
options: {
isHighlighted: true,
},
},
{
title: "Kilde",
type: "object",
name: "kilde",
options: {
isHighlighted: true,
collapsible: true,
},
fields: [
{
title: "Legg til kilde",
name: "har_kilde",
type: "boolean",
initialValue: false,
},
{
title: "Kilde-prefix",
name: "prefix",
type: "string",
options: {
list: [
{ title: "FOTO:", value: "FOTO" },
{ title: "Kilde:", value: "Kilde" },
],
layout: "radio",
direction: "horizontal",
},
hidden: ({ parent }) => !parent?.har_kilde,
},
{
title: "Tekst",
name: "tekst",
type: "string",
hidden: ({ parent }) => !parent?.har_kilde,
},
{
title: "Lenke-kilde",
description: "Kilde-teksten blir satt som lenke",
name: "link",
type: "url",
hidden: ({ parent }) => !parent?.har_kilde,
},
],
},
],
validation: (Rule) =>
Rule.custom((v) => {
return v?.asset ? true : "Må legge til et bilde";
}).error(),
preview: {
select: {
alt: "alt",
floating: "floating",
},
prepare(selection) {
return {
title: selection?.alt,
subtitle: `Bilde`,
media: () => <Picture />,
};
},
},
};
@@ -0,0 +1,89 @@
import { Success } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Do / Dont",
name: "do_dont",
type: "object",
icon: Success,
fields: [
{
type: "array",
name: "blokker",
title: "Do / donts",
of: [{ type: "do_dont_block" }],
validation: (Rule) => Rule.required().max(4),
},
],
preview: {
prepare() {
return {
title: "Do / Dont",
media: () => <Success />,
};
},
},
};
export const doDontBlock = {
title: "DoDont",
name: "do_dont_block",
type: "object",
fields: [
{
title: "Fullwidth",
description: "Tar opp ~ 40% eller 100% av tilgjengelig bredde",
name: "fullwidth",
type: "boolean",
validation: (Rule) => Rule.required(),
initialValue: false,
},
{
title: "Bilde",
name: "picture",
type: "image",
validation: (Rule) =>
Rule.required().error("Do/dont må ha et bilde lagt til"),
options: {
hotspot: true,
},
},
{
title: "alt tekst for bilde",
name: "alt",
type: "string",
validation: (Rule) => Rule.required().error("Do/dont må ha en alt-tekst"),
options: {
isHighlighted: true,
},
},
{
name: "description",
title: "Fritekst",
description:
"Korte konsise beskrivelser. Bruk fullbredde bilde i dodont med egen tekst for lengre forklaringer",
type: "text",
},
{
name: "variant",
title: "Variant",
type: "string",
options: {
list: [
{ title: "Do", value: "do" },
{ title: "Dont", value: "dont" },
{ title: "Warning", value: "warning" },
],
layout: "radio",
},
initialValue: "do",
validation: (Rule) => Rule.required(),
},
],
};
/*
*/
@@ -0,0 +1,36 @@
import { Picture } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Hero bilde",
name: "herobilde",
type: "image",
icon: Picture,
options: {
hotspot: true,
},
fields: [
{
name: "alt",
title: "Alt-tekst",
type: "string",
validation: (Rule) => Rule.required().error("Bilde må ha en alt-tekst"),
description: "Beskriv bildet for skjermlesere",
options: {
isHighlighted: true,
},
},
],
preview: {
select: {
alt: "alt",
},
prepare(selection) {
return {
title: selection?.alt,
subtitle: `Bilde`,
media: () => <Picture />,
};
},
},
};
@@ -0,0 +1,58 @@
import Accordion from "./accordion";
import Alert from "./alert";
import Bilde from "./bilde";
import HeroBilde from "./hero-bilde";
import DoDont, { doDontBlock } from "./do-dont";
import introKomponent from "./intro-komponent";
import Kode from "./kode";
import Props from "./props";
import RelatertInnhold from "./relatert-innhold";
import Innholdskort from "./innholdskort";
import KodeEksempler from "./kode-eksempler";
import {
RiktekstAksel,
RiktekstDsArtikkel,
RiktekstEnkel,
RiktekstKomponent,
RiktekstTabell,
RiktekstPrinsipp,
} from "./riktekst";
import SpesialSeksjon from "./spesial-seksjon";
import Tabell, { TabellSchema } from "./tabell";
import Tastatur from "./tastatur";
import Tips from "./tips";
import Tokens from "./tokens";
import Video from "./video";
const v2Blocks = [
/* Blocks */
RiktekstEnkel,
RiktekstAksel,
RiktekstDsArtikkel,
RiktekstKomponent,
RiktekstTabell,
RiktekstPrinsipp,
/* Moduler */
DoDont,
doDontBlock,
Bilde,
HeroBilde,
Alert,
Kode,
RelatertInnhold,
Innholdskort,
introKomponent,
Tabell,
...TabellSchema,
Props,
Accordion,
SpesialSeksjon,
Video,
Tokens,
Tips,
Tastatur,
KodeEksempler,
];
export default v2Blocks;
@@ -0,0 +1,53 @@
import { FileContent } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Innholdskort",
name: "innholdskort",
type: "object",
fields: [
{
title: "Tittel",
name: "title",
type: "string",
validation: (Rule) =>
Rule.required().error("Innholdskort må ha en beskrivende tittel"),
},
{
title: "Lenke til prinsipp",
name: "lenke",
type: "reference",
to: [{ type: "aksel_prinsipp" }],
validation: (Rule) =>
Rule.required().error("Innholdskort må ha en lenke"),
},
{
title: "Innhold",
description: "Ikke bruk lenker inne i selve kortet",
name: "body",
type: "riktekst_enkel",
validation: (Rule) =>
Rule.required().error("Innholdskort må ha noe beskrivende innhold"),
},
],
validation: (Rule) =>
Rule.custom((_, { document }) => {
if (!document?.prinsipp?.hovedside) {
return "Innholdskort kan bare brukes på 'hovedsiden' til prinsippet";
}
return true;
}),
preview: {
select: {
title: "title",
},
prepare(selection) {
return {
title: selection?.title,
subtitle: `Innholdskort`,
media: () => <FileContent />,
};
},
},
};
@@ -0,0 +1,45 @@
import { Home } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Intro",
name: "intro_komponent",
type: "object",
fields: [
{
title: "Intro-tekst",
name: "body",
type: "riktekst_enkel",
validation: (Rule) =>
Rule.required().error("Komponentintro må ha en intro"),
},
{
type: "array",
name: "brukes_til",
title: "Egnet til å:",
of: [{ type: "string" }],
validation: (Rule) => Rule.required().min(1),
},
{
type: "array",
name: "brukes_ikke_til",
title: "Vurder noe annet: (optional)",
of: [{ type: "string" }],
},
],
options: {
collapsible: true,
collapsed: true,
},
preview: {
select: {
title: "title",
},
prepare() {
return {
title: "Komponent-intro",
media: () => <Home />,
};
},
},
};
@@ -0,0 +1,60 @@
import { Ruler } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Kode-eksempler",
name: "kode_eksempler",
type: "object",
fields: [
{
title: "tittel",
name: "title",
type: "string",
initialValue: "Kode-eksempler",
readOnly: true,
hidden: true,
},
{
title: "Standalone-eksempel",
description: "Vis bare et spesfikt eksempel",
name: "standalone",
type: "boolean",
initialValue: false,
},
{
title: "Installasjon-snippet",
name: "dir",
type: "reference",
to: [{ type: "kode_eksempler_fil" }],
options: {
filter: "dir == true",
},
hidden: ({ parent }) => parent.standalone,
},
{
title: "Installasjon-snippet",
name: "filnavn",
type: "reference",
to: [{ type: "kode_eksempler_fil" }],
options: {
filter: "dir != true",
},
hidden: ({ parent }) => !parent.standalone,
},
],
icon: () => <Ruler />,
preview: {
select: {
standalone: "standalone",
filnavn: "filnavn.title",
dir: "dir.title",
},
prepare({ standalone, filnavn, dir }) {
return {
title: "Komponent-demo",
subtitle: standalone ? filnavn?.replace(".tsx", "") ?? "" : dir ?? "",
media: () => <Ruler />,
};
},
},
};
@@ -0,0 +1,39 @@
import React from "react";
export default {
title: "Kode",
name: "kode",
type: "object",
icon: () => <div>{`<>`}</div>,
fields: [
{
name: "code",
title: "Kode",
type: "code",
hidden: ({ parent }) => parent?.variant,
validation: (Rule) => Rule.required(),
options: {
languageAlternatives: [
{ value: "js", title: "Javascript" },
{ value: "jsx", title: "JSX" },
{ value: "html", title: "HTML" },
{ value: "css", title: "CSS" },
{ value: "less", title: "Less" },
{ value: "terminal", title: "Terminal/Bash" },
{ value: "terminal", title: "Standard" },
],
},
},
],
preview: {
select: {
code: "code",
ref: "ref.title",
},
prepare: ({ code, ref }) => ({
title: code ? `${code?.code?.slice(0, 50)}...` : ref ? ref : "Kode",
subtitle: "kode",
media: () => <div>{`</>`}</div>,
}),
},
};
@@ -0,0 +1,58 @@
import { List } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Props",
name: "props_seksjon",
type: "object",
icon: List,
fields: [
{
title: "Tittel (h2)",
name: "title",
type: "string",
initialValue: "Props",
readOnly: true,
},
{
title: "Props",
type: "array",
name: "komponenter",
of: [
{
title: "Komponent",
type: "object",
name: "komponent",
fields: [
{
title: "Komponent navn",
description: "Slik man ville brukt den, eks Accordion.Item",
type: "string",
name: "title",
},
{
title: "Bruker komponenten OverridableComponent API-et",
type: "boolean",
name: "overridable",
initialValue: false,
},
{
name: "propref",
title: "Komponent referanse",
type: "reference",
to: [{ type: "ds_props" }],
},
],
},
],
},
],
preview: {
prepare() {
return {
title: "Props",
media: () => <List />,
};
},
},
};
@@ -0,0 +1,77 @@
import { allDocumentTypes } from "../../config";
import { Link } from "@navikt/ds-icons";
import React from "react";
export default {
name: "relatert_innhold",
title: "Relatert Innhold",
type: "object",
icon: Link,
fields: [
{
title: "Lenker til innhold",
name: "lenker",
type: "array",
validation: (Rule) =>
Rule.required().max(4).error("Kan ha maks 4 relaterte lenker"),
of: [
{
title: "Lenke",
name: "lenke",
type: "object",
fields: [
{
title: "Tittel",
name: "title",
type: "string",
validation: (Rule) =>
Rule.required()
.max(40)
.error("Tittelen kan være på maks 35 tegn"),
},
{
title: "Intern side i Sanity",
name: "intern",
type: "boolean",
option: {
layout: "checkbox",
},
validation: (Rule) => Rule.required(),
initialValue: false,
},
{
title: "Lenke til Intern sanity-side",
name: "intern_lenke",
type: "reference",
to: [...allDocumentTypes.map((x) => ({ type: x }))],
hidden: ({ parent }) => !parent?.intern,
},
{
title: "Lenke til ekstern side",
name: "ekstern_link",
type: "url",
hidden: ({ parent }) => parent?.intern,
},
{
title: "Linker til et eksternt domene",
description:
"Sett denne hvis lenken går til en side utenfor aksel.nav.no",
name: "ekstern_domene",
type: "boolean",
initialValue: false,
hidden: ({ parent }) => parent?.intern,
},
],
},
],
},
],
preview: {
select: {
links: "links",
},
prepare(s) {
return { title: "Relatert innhold kort", media: () => <Link /> };
},
},
};
@@ -0,0 +1,264 @@
import "@navikt/ds-css?raw";
import "@navikt/ds-css-internal?raw";
import { ExternalLink, FileContent, Link } from "@navikt/ds-icons";
import { BodyLong, Heading } from "@navikt/ds-react/cjs";
import { KBD } from "@sanity/ui";
import React from "react";
import styled from "styled-components";
import { allDocumentTypes } from "../../config";
export const TitleRenderer = (props, size, level) => (
<Heading size={size} level={level}>
{props.children}
</Heading>
);
export const ScCode = styled.code`
color: var(--navds-global-color-deepblue-500);
background-color: var(--navds-global-color-deepblue-50);
border-radius: 6px;
font-size: 1rem;
padding: 2px 0.5rem;
`;
export const styles = [
{
title: "Avsnitt",
value: "normal",
blockEditor: {
render: (props) => <BodyLong>{props.children}</BodyLong>,
},
},
];
export const block = {
title: "Block",
type: "block",
styles: [...styles],
lists: [
{ title: "Bullet", value: "bullet" },
{ title: "Numbered", value: "number" },
],
// Marks let you mark up inline text in the block editor.
marks: {
// Decorators usually describe a single property – e.g. a typographic
// preference or highlighting by editors.
decorators: [
{ title: "Strong", value: "strong" },
{ title: "Emphasis", value: "em" },
{
title: "Code",
value: "code",
blockEditor: {
render: (props) => <ScCode>{props.children}</ScCode>,
},
},
{
title: "KBD",
value: "kbd",
blockEditor: {
icon: () => <div>KBD</div>,
render: (props) => (
<KBD padding={[1, 1, 2]} style={{ verticalAlign: "super" }}>
{props.children}
</KBD>
),
},
},
],
// Annotations can be any object structure – e.g. a link or a footnote.
annotations: [
{
title: "Link til side i sanity",
name: "internalLink",
type: "object",
blockEditor: {
icon: () => <Link />,
},
options: {
modal: {
type: "dialog",
width: "medium", // 'small' | 'medium' | 'large' | 'full'
},
},
fields: [
{
title: "Reference",
name: "reference",
type: "reference",
to: [
...allDocumentTypes.map((doc) => ({
type: doc,
})),
],
},
],
},
{
title: "Lenke",
name: "link",
type: "object",
blockEditor: {
icon: () => <ExternalLink />,
},
fields: [
{
title: "URL",
name: "href",
type: "url",
validation: (Rule) =>
Rule.uri({
scheme: ["https", "mailto", "tel"],
}),
},
{
title: "Åpne siden i ny tab",
description:
"Vi anbefaler å ikke åpne lenker i ny fane slik at brukeren selv kan styre det om ønsket.",
name: "blank",
type: "boolean",
initialValue: false,
},
],
},
],
},
};
export const headingStyles = [
...block.styles,
{
title: "H2",
value: "h2",
blockEditor: {
render: (props) => (
<Heading as="span" size="medium">
{props.children}
</Heading>
),
},
},
{
title: "H3",
value: "h3",
blockEditor: {
render: (props) => (
<Heading as="span" size="small">
{props.children}
</Heading>
),
},
},
{
title: "H4",
value: "h4",
blockEditor: {
render: (props) => (
<Heading as="span" size="xsmall">
{props.children}
</Heading>
),
},
},
];
const Riktekst = (type: "aksel" | "ds" | "komponent" | "prinsipp") => {
const fields: string[] = [];
const standard = [
"relatert_innhold",
"bilde",
"kode",
"tips",
"do_dont",
"accordion",
"alert",
"tabell",
"video",
];
const comp = ["props_seksjon", "tastatur_modul", "tokens", "kode_eksempler"];
const ds_artikkel = ["tokens", "spesial_seksjon"];
fields.push(...standard);
switch (type) {
case "komponent":
fields.push(...comp);
break;
case "ds":
fields.push(...ds_artikkel);
break;
case "prinsipp":
fields.push("innholdskort");
break;
default:
break;
}
const uniq = fields.filter((item, pos, self) => self.indexOf(item) === pos);
return [
{
...block,
styles: [...headingStyles],
},
...uniq.map((x) => ({ type: x })),
];
};
export const RiktekstAksel = {
title: "Riktekst Aksel",
name: "riktekst_aksel",
type: "array",
of: Riktekst("aksel"),
icon: () => <FileContent />,
};
export const RiktekstPrinsipp = {
title: "Riktekst Aksel",
name: "riktekst_prinsipp",
type: "array",
of: Riktekst("prinsipp"),
icon: () => <FileContent />,
};
export const RiktekstDsArtikkel = {
title: "Riktekst Aksel",
name: "riktekst_ds_artikkel",
type: "array",
of: Riktekst("ds"),
icon: () => <FileContent />,
};
export const RiktekstKomponent = {
title: "Riktekst Aksel",
name: "riktekst_komponent",
type: "array",
of: Riktekst("komponent"),
icon: () => <FileContent />,
};
export const RiktekstTabell = {
title: "Riktekst",
name: "riktekst_tabell",
type: "array",
of: [
{
...block,
marks: {
...block.marks,
annotations: block.marks.annotations.filter(
(x) => x.name !== "internalLink"
),
},
},
],
};
export const RiktekstEnkel = {
title: "Riktekst",
name: "riktekst_enkel",
type: "array",
of: [block],
};
@@ -0,0 +1,45 @@
import { Star } from "@navikt/ds-icons";
import React from "react";
export default {
title: "Sidemodul",
type: "object",
name: "spesial_seksjon",
icon: Star,
validation: (Rule) => Rule.required(),
fields: [
{
title: "Modul",
name: "modul",
type: "string",
options: {
list: [
{ title: "Farge kategori", value: "farge_kategori" },
{ title: "Ikonsøk", value: "ikonsok" },
{ title: "Endringslogg", value: "endringslogg" },
{ title: "Komponentoversikt", value: "komponentoversikt" },
],
layout: "radio",
},
},
{
title: "Farge kategori",
type: "reference",
name: "farge_ref",
hidden: ({ parent }) => parent.modul !== "farge_kategori",
to: [{ type: "ds_color_categories" }],
},
],
preview: {
select: {
modul: "modul",
},
prepare(s) {
return {
title: s.modul,
subtitle: "Spesial seksjon",
media: () => <Star />,
};
},
},
};
Loaded 100 of 494 files, more files were not shown because too many files have changed in this diff. Show more