mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 09:12:03 +00:00
Compare commits
200
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51bbeb00b0 | ||
|
|
79773ff44b | ||
|
|
1e7633e692 | ||
|
|
ba22ba7b70 | ||
|
|
ccedba0132 | ||
|
|
2dbc5125d6 | ||
|
|
bdf83d447f | ||
|
|
da9445389e | ||
|
|
c0601bb510 | ||
|
|
e20df992ec | ||
|
|
d42200b61f | ||
|
|
4e5f422693 | ||
|
|
2c77e103f9 | ||
|
|
45e9e5f564 | ||
|
|
6b1f9f1560 | ||
|
|
eb50604788 | ||
|
|
5ca140d4f7 | ||
|
|
7943489380 | ||
|
|
82cdd68d00 | ||
|
|
76af311b63 | ||
|
|
b624bff400 | ||
|
|
d5ab822973 | ||
|
|
c8b230fc1c | ||
|
|
d35f7326d7 | ||
|
|
eb07f63d12 | ||
|
|
df42875d41 | ||
|
|
404274003c | ||
|
|
fc9c5170b9 | ||
|
|
dc2f1958ff | ||
|
|
fc9196ad6f | ||
|
|
9cfc03fe6b | ||
|
|
9b8ed2d985 | ||
|
|
728dd2ead3 | ||
|
|
71fbe7d7f2 | ||
|
|
2a4447b6e7 | ||
|
|
aaad005455 | ||
|
|
3bb4a2e36f | ||
|
|
f346a4694c | ||
|
|
51c0883255 | ||
|
|
45e67d5b50 | ||
|
|
6739d29074 | ||
|
|
189858a9d7 | ||
|
|
f1731730fa | ||
|
|
1b37aad4dc | ||
|
|
323018a935 | ||
|
|
7b6e5f2e60 | ||
|
|
5557fc2ef0 | ||
|
|
6c07afe963 | ||
|
|
ac217adaa6 | ||
|
|
5c50658036 | ||
|
|
8ab100dfb3 | ||
|
|
3ac20c695c | ||
|
|
218707905e | ||
|
|
72f1ab0c85 | ||
|
|
2c57117098 | ||
|
|
f6ba5e1219 | ||
|
|
1c53160a2a | ||
|
|
22527c073f | ||
|
|
5b41ecac4c | ||
|
|
14e07e4ca6 | ||
|
|
cd1c93f9d4 | ||
|
|
545864553f | ||
|
|
3b2f10fb91 | ||
|
|
bec7d5945f | ||
|
|
f32bd408a2 | ||
|
|
7ba8c466f4 | ||
|
|
bede18133c | ||
|
|
81e65cc0ab | ||
|
|
5acdbe33c1 | ||
|
|
727999ba72 | ||
|
|
7ad82450b4 | ||
|
|
a72e965785 | ||
|
|
e3fd48728b | ||
|
|
89c3603765 | ||
|
|
03f7034fca | ||
|
|
527ba00287 | ||
|
|
b3831f0b1c | ||
|
|
0549ad19b6 | ||
|
|
7d1095fd96 | ||
|
|
acb1d0b955 | ||
|
|
4fc51e00b5 | ||
|
|
796bb5225f | ||
|
|
9ff2830297 | ||
|
|
d40939458a | ||
|
|
310974b3c3 | ||
|
|
73b30a8ea7 | ||
|
|
790e70a90d | ||
|
|
4114cecd93 | ||
|
|
797f48a9fb | ||
|
|
13d3007293 | ||
|
|
bcfd6747d9 | ||
|
|
122bbf0090 | ||
|
|
c0cb5a02b3 | ||
|
|
b60d047b95 | ||
|
|
98dbc155fe | ||
|
|
87f194c1dc | ||
|
|
7f73315264 | ||
|
|
66badd5df5 | ||
|
|
7360426aef | ||
|
|
b71d407396 | ||
|
|
ca7c4964a2 | ||
|
|
5e078db3fd | ||
|
|
eb32b0a80b | ||
|
|
b56f83bf50 | ||
|
|
1399f6f084 | ||
|
|
1cf90c1753 | ||
|
|
c027c2e464 | ||
|
|
92dd582d68 | ||
|
|
b55bfac1c3 | ||
|
|
bb302bbbd6 | ||
|
|
55005c80f3 | ||
|
|
27e6423b54 | ||
|
|
de2dcdcc15 | ||
|
|
12e95d063a | ||
|
|
643ae3bc15 | ||
|
|
21a842d419 | ||
|
|
73f5f9fbfd | ||
|
|
527fe4f0c1 | ||
|
|
1a9214e328 | ||
|
|
8c0b2363e0 | ||
|
|
e7a62a9082 | ||
|
|
494566e604 | ||
|
|
1c492fda56 | ||
|
|
9691ac4713 | ||
|
|
7174bbe7bf | ||
|
|
62f36da5e7 | ||
|
|
9ec5133ba2 | ||
|
|
0a7906187f | ||
|
|
28f2a3f532 | ||
|
|
f5d404b925 | ||
|
|
05955acaf7 | ||
|
|
55362f3ea7 | ||
|
|
28f8f7a408 | ||
|
|
6778e902f3 | ||
|
|
92566d5750 | ||
|
|
6911913748 | ||
|
|
7f3e96ae01 | ||
|
|
19791c0339 | ||
|
|
6d38b7bbdd | ||
|
|
936e794e2f | ||
|
|
6a00fca823 | ||
|
|
500eecc43d | ||
|
|
2ac73a7812 | ||
|
|
451dacb14a | ||
|
|
e6228c103d | ||
|
|
572d913718 | ||
|
|
c0cd0dd66f | ||
|
|
613e20eb12 | ||
|
|
4bf0506362 | ||
|
|
5997db2b38 | ||
|
|
f5d3ed8eee | ||
|
|
5c00b91dfb | ||
|
|
9f18063fe5 | ||
|
|
b037114863 | ||
|
|
e47521d6e5 | ||
|
|
61357fb40a | ||
|
|
5678d4cf10 | ||
|
|
d403c09b1a | ||
|
|
5b82182fda | ||
|
|
6246c1d311 | ||
|
|
04ad4d09b2 | ||
|
|
65ddd4bd18 | ||
|
|
deff5b25b2 | ||
|
|
31e60deea8 | ||
|
|
847142dc72 | ||
|
|
a5b3ad81a7 | ||
|
|
fe1ac779bf | ||
|
|
8bb5b8f7b9 | ||
|
|
064b2fc612 | ||
|
|
dcf5cfc06b | ||
|
|
ff97e7f3f8 | ||
|
|
9e1165de0a | ||
|
|
812573ac43 | ||
|
|
5f340eba49 | ||
|
|
703e7e3ab5 | ||
|
|
b42101f6f1 | ||
|
|
8b0c4d8f59 | ||
|
|
6ab84e581f | ||
|
|
42f3a298a2 | ||
|
|
e49922d3fe | ||
|
|
fc06e8f7b4 | ||
|
|
0273b42fb4 | ||
|
|
0873fe6520 | ||
|
|
fdcf502a64 | ||
|
|
aebb22bf1b | ||
|
|
4529e64c6e | ||
|
|
01395e65ba | ||
|
|
21dd0450e7 | ||
|
|
f51d43c06a | ||
|
|
8c81ae46e0 | ||
|
|
d64da78d8b | ||
|
|
21da657eb0 | ||
|
|
52d2d5f41d | ||
|
|
ff2515fabb | ||
|
|
5d57586dc1 | ||
|
|
e8d012b82d | ||
|
|
4e90102efd | ||
|
|
02c55aaa0f | ||
|
|
8d4ee29d78 | ||
|
|
23066e9503 |
No files matched your search
@@ -0,0 +1,24 @@
|
||||
name: Ny komponent
|
||||
description: Send et ønske om en eller flere nye komponenter
|
||||
title: "[Ny komponent]: "
|
||||
labels: ["nytt ✨", "forespørsel 🥰", "komponenter 🧩"]
|
||||
assignees:
|
||||
- olejorgenbakken
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Takk for at du bidrar inn i Aksel!
|
||||
- type: input
|
||||
id: title
|
||||
attributes:
|
||||
label: Navn på komponent
|
||||
description: Hva er navnet på komponenten du ønsker deg?
|
||||
placeholder: eks. datepicker
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: Eksempel på komponenten
|
||||
description: Send en lenke til eksempler på komponenten
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Nytt ikon
|
||||
description: Send et ønske om et eller flere nye ikoner
|
||||
title: "[Nytt ikon]: "
|
||||
labels: ["nytt ✨", "ikoner 🖼", "forespørsel 🥰"]
|
||||
assignees:
|
||||
- olejorgenbakken
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Takk for at du bidrar inn i Aksel!
|
||||
- type: input
|
||||
id: title
|
||||
attributes:
|
||||
label: Navn på ikonet
|
||||
description: Hva er navnet på ikonet du ønsker deg?
|
||||
placeholder: eks. send
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: Eksempel på ikonet
|
||||
description: Send en lenke til eksempler på ikonet
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Innspill til komponent
|
||||
description: Send inn innspill til komponent
|
||||
title: "[Innspill til komponent]: "
|
||||
labels: ["forespørsel 🥰", "komponenter 🧩", "nytt ✨", "beta 🧪"]
|
||||
assignees:
|
||||
- olejorgenbakken
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Takk for at du bidrar inn i Aksel!
|
||||
- type: textarea
|
||||
id: feeback
|
||||
attributes:
|
||||
label: Tilbakemelding
|
||||
description: Beskriv hvilke tilbakemeldinger du har til komponenten
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Innspill til ikon
|
||||
description: Send inn innspill til ikon
|
||||
title: "[Innspill til ikon]: "
|
||||
labels: ["forespørsel 🥰", "nytt ✨"]
|
||||
assignees:
|
||||
- olejorgenbakken
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Takk for at du bidrar inn i Aksel!
|
||||
- type: textarea
|
||||
id: feeback
|
||||
attributes:
|
||||
label: Tilbakemelding
|
||||
description: Beskriv hvilke tilbakemeldinger du har til ikonet
|
||||
@@ -9,6 +9,34 @@ env:
|
||||
IMAGE: ghcr.io/navikt/designsystemet
|
||||
|
||||
jobs:
|
||||
snyk-monitor:
|
||||
name: monitor dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/node@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
command: monitor
|
||||
args: --org=designsystemet --severity-threshold=high
|
||||
snyk-test:
|
||||
name: analyze dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
command: test
|
||||
args: --org=designsystemet --severity-threshold=high
|
||||
build_and_deploy:
|
||||
name: Build and deploy to dev-gcp
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -17,8 +45,6 @@ jobs:
|
||||
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
|
||||
@@ -40,11 +66,10 @@ jobs:
|
||||
- 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
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # Use commit-sha1 instead of tag for security concerns
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -10,9 +10,68 @@ env:
|
||||
IMAGE: ghcr.io/navikt/designsystemet
|
||||
|
||||
jobs:
|
||||
snyk-monitor:
|
||||
name: monitor dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/node@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
command: monitor
|
||||
args: --org=designsystemet --severity-threshold=high
|
||||
snyk-test:
|
||||
name: analyze dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
command: test
|
||||
args: --org=designsystemet --severity-threshold=high
|
||||
sync_data_with_sanity:
|
||||
name: Update component data
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
|
||||
- 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: Run docgen
|
||||
run: |
|
||||
yarn boot
|
||||
yarn docgen
|
||||
|
||||
- name: Update sanity sync
|
||||
run: yarn workspace aksel.nav.no sanity:update
|
||||
|
||||
build_and_deploy:
|
||||
name: Build and deploy to prod-gcp
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
@@ -36,25 +95,17 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: build packages
|
||||
run: |
|
||||
yarn boot
|
||||
yarn docgen
|
||||
- name: Build packages
|
||||
run: yarn boot
|
||||
|
||||
- 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
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # Use commit-sha1 instead of tag for security concerns
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -65,6 +116,13 @@ jobs:
|
||||
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: Wait for e2e-tests to succeed
|
||||
uses: lewagon/wait-on-check-action@v1.2.0
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
check-name: "Smoketest Aksel"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
- name: Push docker image
|
||||
run: |
|
||||
docker push ${{ env.DOCKER_TAG }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
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
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # Use commit-sha1 instead of tag for security concerns
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
run: cd aksel.nav.no/website && yarn
|
||||
|
||||
- name: Export dataset
|
||||
run: cd aksel.nav.no/website && SANITY_TOKEN=${{secrets.SANITY_DATASET_EDITOR_TOKEN}} yarn backup
|
||||
run: cd aksel.nav.no/website && SANITY_PREVIEW_TOKEN=${{secrets.SANITY_DATASET_EDITOR_TOKEN}} yarn backup
|
||||
- name: Upload GCP bucket
|
||||
uses: "google-github-actions/auth@v0"
|
||||
with:
|
||||
|
||||
@@ -12,6 +12,8 @@ on:
|
||||
jobs:
|
||||
playwright:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.30.0-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
@@ -36,15 +38,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: build packages
|
||||
- name: Build packages
|
||||
run: |
|
||||
yarn boot
|
||||
yarn docgen
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Build application
|
||||
run: yarn workspace aksel.nav.no build:next
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: yarn workspace website e2e:full
|
||||
run: yarn workspace website e2e:ci-full
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Smoketest aksel.nav.no
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "aksel.nav.no/**"
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/navikt/designsystemet
|
||||
|
||||
jobs:
|
||||
smoketest:
|
||||
name: Smoketest Aksel
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.30.0-focal
|
||||
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 "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
|
||||
|
||||
- name: Build application
|
||||
run: yarn workspace aksel.nav.no build:next
|
||||
|
||||
- name: E2E smoketest
|
||||
run: yarn workspace website e2e:ci
|
||||
@@ -1,5 +1,19 @@
|
||||
# @navikt/ds-codemod
|
||||
|
||||
## 2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
## 2.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
## 2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-codemod",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "Aksel codemods for NAVs designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
"build-pict": "svgr --silent --index-template index-template.js --out-dir src/pictograms pictograms"
|
||||
},
|
||||
"dependencies": {
|
||||
"@navikt/ds-icons": "^2.0.18",
|
||||
"@navikt/ds-react": "^2.0.18",
|
||||
"@navikt/ds-icons": "^2.1.6",
|
||||
"@navikt/ds-react": "^2.1.6",
|
||||
"clsx": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [`62f36da5e`](https://github.com/navikt/Designsystemet/commit/62f36da5e74fc7f53414d42b310c3b57597795bc) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Small textfield har nå 8px horisontal padding (4px før)
|
||||
|
||||
## 2.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
## 2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-text-field__input {
|
||||
padding: 0 0.25rem;
|
||||
padding: 0 0.5rem;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "CSS for NAV Designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -20,7 +20,7 @@
|
||||
"watch": "postcss --watch --use postcss-import -o dist/index.css index.css"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^2.0.18",
|
||||
"@navikt/ds-tokens": "^2.1.6",
|
||||
"normalize.css": "^8.0.1",
|
||||
"postcss": "^8.4.0",
|
||||
"postcss-cli": "^9.0.0",
|
||||
|
||||
@@ -1,5 +1,68 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1777](https://github.com/navikt/Designsystemet/pull/1777) [`cd1c93f9d`](https://github.com/navikt/Designsystemet/commit/cd1c93f9d471fbbc38215d788a73f21b5b216c5b) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Datepicker og monthpicker kan nå bestemme om popover skal dukke opp ved fokus
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-icons@2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Hotfix: ESM import fra date-fns feilet
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-icons@2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [`87f194c1d`](https://github.com/navikt/Designsystemet/commit/87f194c1dc29b10a604de676b358d117e2dbf52f) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset edgecase i useRangepicker der valg av startdato etter sluttdato ga feil output
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-icons@2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1771](https://github.com/navikt/Designsystemet/pull/1771) [`494566e60`](https://github.com/navikt/Designsystemet/commit/494566e604e958363f75ebfa2adbc1669397636a) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Monthpicker håndterer nå visning av År riktig
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-icons@2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-icons@2.1.2
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1759](https://github.com/navikt/Designsystemet/pull/1759) [`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset @types/react v18 feil introdusert i v2.0.6
|
||||
|
||||
- Updated dependencies [[`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57)]:
|
||||
- @navikt/ds-icons@2.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [`0873fe652`](https://github.com/navikt/Designsystemet/commit/0873fe6520e817e462197c162b49c05826da4838) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Datepicker/Monthpicker støtter nå år med 2 siffer i input
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-icons@2.1.0
|
||||
|
||||
## 2.0.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "Aksel react-components for NAV designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.17.0",
|
||||
"@navikt/ds-icons": "^2.0.18",
|
||||
"@navikt/ds-icons": "^2.1.6",
|
||||
"@radix-ui/react-tabs": "1.0.0",
|
||||
"@radix-ui/react-toggle-group": "1.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
|
||||
@@ -148,6 +148,7 @@ export const UseDatepicker = () => {
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
onDateChange: console.log,
|
||||
locale: "en",
|
||||
openOnFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -164,6 +165,7 @@ export const UseRangedDatepicker = () => {
|
||||
UNSAFE_useRangeDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
onRangeChange: console.log,
|
||||
onValidate: console.log,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -178,6 +180,21 @@ export const UseRangedDatepicker = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenOnFocus = () => {
|
||||
const { datepickerProps, inputProps } = UNSAFE_useDatepicker({
|
||||
onDateChange: console.log,
|
||||
openOnFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NB = () => (
|
||||
<DatePicker.Standalone locale="nb" today={new Date("2006-07-01")} />
|
||||
);
|
||||
|
||||
@@ -49,6 +49,18 @@ export interface UseDatepickerOptions
|
||||
* validation-callback
|
||||
*/
|
||||
onValidate?: (val: DateValidationT) => void;
|
||||
/**
|
||||
* Allows input of with 'yy' year format.
|
||||
* @default true
|
||||
* @Note Decision between 20th and 21st century is based on before(todays year - 80) ? 21st : 20th.
|
||||
* In 2023 this equals to 1943 - 2042
|
||||
*/
|
||||
allowTwoDigitYear?: boolean;
|
||||
/**
|
||||
* Opens datepicker on input-focus
|
||||
* @default true
|
||||
*/
|
||||
openOnFocus?: boolean;
|
||||
}
|
||||
|
||||
interface UseDatepickerValue {
|
||||
@@ -115,6 +127,8 @@ export const useDatepicker = (
|
||||
inputFormat,
|
||||
onValidate,
|
||||
defaultMonth,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const locale = getLocaleFromString(_locale);
|
||||
@@ -190,8 +204,14 @@ export const useDatepicker = (
|
||||
};
|
||||
|
||||
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
!open && setOpen(true);
|
||||
let day = parseDate(e.target.value, today, locale, "date");
|
||||
!open && openOnFocus && setOpen(true);
|
||||
let day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"date",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
if (isValidDate(day)) {
|
||||
setMonth(day);
|
||||
setInputValue(formatDateForInput(day, locale, "date", inputFormat));
|
||||
@@ -199,7 +219,13 @@ export const useDatepicker = (
|
||||
};
|
||||
|
||||
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
let day = parseDate(e.target.value, today, locale, "date");
|
||||
let day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"date",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
isValidDate(day) &&
|
||||
setInputValue(formatDateForInput(day, locale, "date", inputFormat));
|
||||
};
|
||||
@@ -230,7 +256,13 @@ export const useDatepicker = (
|
||||
// the calendar’s month.
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setInputValue(e.target.value);
|
||||
const day = parseDate(e.target.value, today, locale, "date");
|
||||
const day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"date",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
|
||||
const isBefore =
|
||||
fromDate && day && differenceInCalendarDays(fromDate, day) > 0;
|
||||
|
||||
@@ -35,6 +35,18 @@ export interface UseMonthPickerOptions
|
||||
* Default shown year
|
||||
*/
|
||||
defaultYear?: Date;
|
||||
/**
|
||||
* Allows input of with 'yy' year format.
|
||||
* @default true
|
||||
* @Note Decision between 20th and 21st century is based on before(todays year - 80) ? 21st : 20th.
|
||||
* In 2023 this equals to 1943 - 2042
|
||||
*/
|
||||
allowTwoDigitYear?: boolean;
|
||||
/**
|
||||
* Opens datepicker on input-focus
|
||||
* @default true
|
||||
*/
|
||||
openOnFocus?: boolean;
|
||||
}
|
||||
|
||||
interface UseMonthPickerValue {
|
||||
@@ -96,6 +108,8 @@ export const useMonthpicker = (
|
||||
inputFormat,
|
||||
onValidate,
|
||||
defaultYear,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
|
||||
@@ -172,16 +186,30 @@ export const useMonthpicker = (
|
||||
};
|
||||
|
||||
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
!open && setOpen(true);
|
||||
let day = parseDate(e.target.value, today, locale, "month");
|
||||
!open && openOnFocus && setOpen(true);
|
||||
let day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"month",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
if (isValidDate(day)) {
|
||||
setYear(day);
|
||||
setInputValue(formatDateForInput(day, locale, "month", inputFormat));
|
||||
} else {
|
||||
setYear(defaultSelected ?? defaultYear ?? today);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
let day = parseDate(e.target.value, today, locale, "month");
|
||||
let day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"month",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
isValidDate(day) &&
|
||||
setInputValue(formatDateForInput(day, locale, "month", inputFormat));
|
||||
};
|
||||
@@ -191,12 +219,14 @@ export const useMonthpicker = (
|
||||
if (month) {
|
||||
setOpen(false);
|
||||
inputRef.current && inputRef.current.focus();
|
||||
setYear(month);
|
||||
}
|
||||
|
||||
if (!required && !month) {
|
||||
updateMonth(undefined);
|
||||
updateValidation({ isValidMonth: false, isEmpty: true });
|
||||
setInputValue("");
|
||||
setYear(defaultYear ?? today);
|
||||
return;
|
||||
}
|
||||
updateMonth(month);
|
||||
@@ -211,7 +241,13 @@ export const useMonthpicker = (
|
||||
// the calendar’s month.
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setInputValue(e.target.value);
|
||||
const month = parseDate(e.target.value, today, locale, "month");
|
||||
const month = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"month",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
|
||||
const isBefore =
|
||||
fromDate &&
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
||||
import isBefore from "date-fns/isBefore";
|
||||
import isWeekend from "date-fns/isWeekend";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { DateRange, isMatch } from "react-day-picker";
|
||||
@@ -202,6 +203,8 @@ export const useRangeDatepicker = (
|
||||
inputFormat,
|
||||
onValidate,
|
||||
defaultMonth,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const locale = getLocaleFromString(_locale);
|
||||
@@ -321,8 +324,14 @@ export const useRangeDatepicker = (
|
||||
};
|
||||
|
||||
const handleFocus = (e, src: RangeT) => {
|
||||
!open && setOpen(true);
|
||||
let day = parseDate(e.target.value, today, locale, "date");
|
||||
!open && openOnFocus && setOpen(true);
|
||||
let day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"date",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
if (isValidDate(day)) {
|
||||
setMonth(day);
|
||||
src === RANGE.FROM
|
||||
@@ -334,7 +343,13 @@ export const useRangeDatepicker = (
|
||||
};
|
||||
|
||||
const handleBlur = (e, src: RangeT) => {
|
||||
let day = parseDate(e.target.value, today, locale, "date");
|
||||
let day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"date",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
if (!isValidDate(day)) {
|
||||
return;
|
||||
}
|
||||
@@ -358,24 +373,26 @@ export const useRangeDatepicker = (
|
||||
if (range?.from && range?.to) {
|
||||
setOpen(false);
|
||||
}
|
||||
const prevToRange =
|
||||
let prevToRange =
|
||||
!selectedRange?.from && selectedRange?.to ? selectedRange?.to : range?.to;
|
||||
|
||||
const resetTo = isBefore(prevToRange, range?.from);
|
||||
|
||||
range?.from
|
||||
? setFromInputValue(
|
||||
formatDateForInput(range?.from, locale, "date", inputFormat)
|
||||
)
|
||||
: setFromInputValue("");
|
||||
prevToRange
|
||||
prevToRange && !resetTo
|
||||
? setToInputValue(
|
||||
formatDateForInput(prevToRange, locale, "date", inputFormat)
|
||||
)
|
||||
: setToInputValue("");
|
||||
updateValidation(
|
||||
{ isValidDate: !!range?.from, isEmpty: !range?.from },
|
||||
{ isValidDate: !!range?.to, isEmpty: !prevToRange }
|
||||
{ isValidDate: !!range?.to, isEmpty: !prevToRange || resetTo }
|
||||
);
|
||||
updateRange({ from: range?.from, to: prevToRange });
|
||||
updateRange({ from: range?.from, to: resetTo ? undefined : prevToRange });
|
||||
};
|
||||
|
||||
const fromChange = (
|
||||
@@ -426,7 +443,13 @@ export const useRangeDatepicker = (
|
||||
}
|
||||
|
||||
if (toInputValue && !selectedRange?.to) {
|
||||
const toDay = parseDate(toInputValue, today, locale, "date");
|
||||
const toDay = parseDate(
|
||||
toInputValue,
|
||||
today,
|
||||
locale,
|
||||
"date",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
if (validateDay(toDay)) {
|
||||
updateRange({ from: day, to: toDay });
|
||||
setMonth(day);
|
||||
@@ -489,7 +512,13 @@ export const useRangeDatepicker = (
|
||||
|
||||
/* live-update datepicker based on changes in inputfields */
|
||||
const handleChange = (e, src: RangeT) => {
|
||||
const day = parseDate(e.target.value, today, locale, "date");
|
||||
const day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
locale,
|
||||
"date",
|
||||
allowTwoDigitYear
|
||||
);
|
||||
const isBefore = fromDate && differenceInCalendarDays(fromDate, day) > 0;
|
||||
const isAfter = toDate && differenceInCalendarDays(day, toDate) > 0;
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
|
||||
);
|
||||
|
||||
const handleSelect = (month?: Date) => {
|
||||
setSelectedMonth(month);
|
||||
!onMonthSelect && setSelectedMonth(month);
|
||||
onMonthSelect?.(month);
|
||||
month && (onClose?.() ?? setOpen(false));
|
||||
};
|
||||
@@ -175,7 +175,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
|
||||
>
|
||||
<RootProvider
|
||||
locale={getLocaleFromString(locale)}
|
||||
selected={selected}
|
||||
selected={selected ?? selectedMonth}
|
||||
toDate={toDate}
|
||||
fromDate={fromDate}
|
||||
month={selected ?? selectedMonth}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { parseDate } from "../parse-date";
|
||||
import nb from "date-fns/locale/nb";
|
||||
import { formatDateForInput } from "../format-date";
|
||||
|
||||
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date");
|
||||
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
|
||||
const parseTwoDigit = (inp: string) =>
|
||||
parseDate(inp, new Date(), nb, "date", true);
|
||||
|
||||
describe("Format date to correct output", () => {
|
||||
test("formatDateForInput", () => {
|
||||
@@ -19,4 +21,12 @@ describe("Format date to correct output", () => {
|
||||
"15.05.2022"
|
||||
);
|
||||
});
|
||||
test("formatDateForInput with twoDigitYears", () => {
|
||||
expect(formatDateForInput(parseTwoDigit("15/05/22"), nb, "date")).toEqual(
|
||||
"15.05.2022"
|
||||
);
|
||||
expect(formatDateForInput(parseTwoDigit("1/5/95"), nb, "date")).toEqual(
|
||||
"01.05.1995"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,11 +4,14 @@ import nb from "date-fns/locale/nb";
|
||||
import getMonth from "date-fns/getMonth";
|
||||
|
||||
const check = (inp: string) =>
|
||||
expect(isValidDate(parseDate(inp, new Date(), nb, "date")));
|
||||
expect(isValidDate(parseDate(inp, new Date(), nb, "date", false)));
|
||||
|
||||
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date");
|
||||
const checkTwoDigit = (inp: string) =>
|
||||
expect(isValidDate(parseDate(inp, new Date(), nb, "date", true)));
|
||||
|
||||
describe("Parse date-inputs", () => {
|
||||
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
|
||||
|
||||
describe("Parse date-inputs with 4-digit years", () => {
|
||||
test("No spaces", () => {
|
||||
check("11052022").toBeTruthy();
|
||||
check("15052022").toBeTruthy();
|
||||
@@ -34,6 +37,44 @@ describe("Parse date-inputs", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse date-inputs with 2-digit years", () => {
|
||||
test("No spaces", () => {
|
||||
checkTwoDigit("110522").toBeTruthy();
|
||||
checkTwoDigit("150522").toBeTruthy();
|
||||
});
|
||||
|
||||
test(". divider", () => {
|
||||
checkTwoDigit("1.5.22").toBeTruthy();
|
||||
checkTwoDigit("11.05.22").toBeTruthy();
|
||||
});
|
||||
|
||||
test("/ divider", () => {
|
||||
checkTwoDigit("1/5/22").toBeTruthy();
|
||||
checkTwoDigit("10/5/22").toBeTruthy();
|
||||
checkTwoDigit("1/05/22").toBeTruthy();
|
||||
checkTwoDigit("10/05/22").toBeTruthy();
|
||||
});
|
||||
|
||||
test("- divider", () => {
|
||||
checkTwoDigit("1-5-22").toBeTruthy();
|
||||
checkTwoDigit("10-5-22").toBeTruthy();
|
||||
checkTwoDigit("1-05-22").toBeTruthy();
|
||||
checkTwoDigit("10-05-22").toBeTruthy();
|
||||
});
|
||||
|
||||
test("Dissallow 1 and 3 digit years", () => {
|
||||
checkTwoDigit("11052").toBeFalsy();
|
||||
checkTwoDigit("1105222").toBeFalsy();
|
||||
checkTwoDigit("1105999").toBeFalsy();
|
||||
});
|
||||
|
||||
test("Dissallow date before year 1000", () => {
|
||||
checkTwoDigit("11050999").toBeFalsy();
|
||||
checkTwoDigit("11050010").toBeFalsy();
|
||||
checkTwoDigit("11051000").toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse date-inputs to correct month", () => {
|
||||
test("No spaces", () => {
|
||||
expect(getMonth(parse("11052022"))).toEqual(4);
|
||||
|
||||
@@ -10,7 +10,7 @@ export const dateIsInCurrentMonth = (
|
||||
|
||||
/** @private */
|
||||
export function isValidDate(day: Date): boolean {
|
||||
return !isNaN(day.getTime());
|
||||
return day && !isNaN(day?.getTime()) && day.getFullYear() > 999;
|
||||
}
|
||||
|
||||
export const hasNextYear = (year: Date, years: Date[], val: any): boolean => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import parse from "date-fns/parse";
|
||||
import isBefore from "date-fns/isBefore";
|
||||
import sub from "date-fns/sub";
|
||||
import { isValidDate } from ".";
|
||||
|
||||
export const INPUT_DATE_STRING_FORMAT_DATE = "dd.MM.yyyy";
|
||||
@@ -22,7 +24,60 @@ const ALLOWED_INPUT_FORMATS_MONTH = [
|
||||
...ALLOWED_INPUT_FORMATS_DATE,
|
||||
];
|
||||
|
||||
const isTwoDigitYear = (dateString, today, locale, formats) => {
|
||||
export const parseDate = (
|
||||
date: string,
|
||||
today: Date,
|
||||
locale: Locale,
|
||||
type: "date" | "month",
|
||||
allowTwoDigitYear: boolean
|
||||
): Date => {
|
||||
let parsed;
|
||||
const ALLOWED_FORMATS =
|
||||
type === "date" ? ALLOWED_INPUT_FORMATS_DATE : ALLOWED_INPUT_FORMATS_MONTH;
|
||||
|
||||
if (allowTwoDigitYear) {
|
||||
for (const format of ALLOWED_FORMATS) {
|
||||
parsed = parse(date, format, today, { locale });
|
||||
if (
|
||||
isValidDate(parsed) &&
|
||||
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
for (const format of [
|
||||
...ALLOWED_FORMATS.map((x) => x.replace("yyyy", "yy")),
|
||||
]) {
|
||||
parsed = parse(date, format, today, { locale });
|
||||
if (
|
||||
isValidDate(parsed) &&
|
||||
isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
|
||||
) {
|
||||
const convertedDate = assignCenturyToDate(date, format, today, locale);
|
||||
|
||||
if (isValidDate(new Date(convertedDate))) {
|
||||
return new Date(convertedDate);
|
||||
} else {
|
||||
return new Date("Invalid date");
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Date("Invalid date");
|
||||
} else {
|
||||
for (const format of ALLOWED_FORMATS) {
|
||||
parsed = parse(date, format, today, { locale });
|
||||
if (
|
||||
isValidDate(parsed) &&
|
||||
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return new Date("Invalid date");
|
||||
}
|
||||
};
|
||||
|
||||
function isTwoDigitYear(dateString, today, locale, formats) {
|
||||
let parsed;
|
||||
const newFormat = formats.map((x) => x.replace("yyyy", "yy"));
|
||||
for (const format of newFormat) {
|
||||
@@ -32,25 +87,49 @@ const isTwoDigitYear = (dateString, today, locale, formats) => {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export const parseDate = (
|
||||
date: string,
|
||||
function assignCenturyToDate(
|
||||
dateStr: string,
|
||||
format: string,
|
||||
today: Date,
|
||||
locale: Locale,
|
||||
type: "date" | "month"
|
||||
): Date => {
|
||||
let parsed;
|
||||
const ALLOWED_FORMATS =
|
||||
type === "date" ? ALLOWED_INPUT_FORMATS_DATE : ALLOWED_INPUT_FORMATS_MONTH;
|
||||
for (const format of ALLOWED_FORMATS) {
|
||||
parsed = parse(date, format, today, { locale });
|
||||
if (
|
||||
isValidDate(parsed) &&
|
||||
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
locale: Locale
|
||||
) {
|
||||
const date20Century = parse(
|
||||
appendCenturyToTwoYearDigitDateString(dateStr, "19"),
|
||||
format.replace("yy", "yyyy"),
|
||||
today,
|
||||
{ locale }
|
||||
);
|
||||
|
||||
const date21Century = parse(
|
||||
appendCenturyToTwoYearDigitDateString(dateStr, "20"),
|
||||
format.replace("yy", "yyyy"),
|
||||
today,
|
||||
{ locale }
|
||||
);
|
||||
|
||||
if (isValidDate(date20Century) && isValidDate(date21Century)) {
|
||||
return isBefore(
|
||||
date20Century,
|
||||
sub(new Date(), {
|
||||
years: 80,
|
||||
})
|
||||
)
|
||||
? date21Century
|
||||
: date20Century;
|
||||
}
|
||||
|
||||
return new Date("Invalid date");
|
||||
};
|
||||
}
|
||||
|
||||
function appendCenturyToTwoYearDigitDateString(
|
||||
dateString: string,
|
||||
century: "19" | "20"
|
||||
) {
|
||||
const twoDigitYear = dateString.slice(-2);
|
||||
return `${dateString.slice(
|
||||
0,
|
||||
dateString.length - 2
|
||||
)}${century}${twoDigitYear}`;
|
||||
}
|
||||
@@ -6,11 +6,15 @@ interface SVGRProps {
|
||||
titleId?: string;
|
||||
}
|
||||
|
||||
export const DefaultIllustration = ({
|
||||
export type DefaultIllustrationType = React.FunctionComponent<
|
||||
SVGProps<SVGSVGElement> & SVGRProps
|
||||
>;
|
||||
|
||||
export const DefaultIllustration: DefaultIllustrationType = ({
|
||||
title,
|
||||
titleId: _titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps) => {
|
||||
}) => {
|
||||
let titleId: string | undefined = useId();
|
||||
titleId = title ? (_titleId ? _titleId : "title-" + titleId) : undefined;
|
||||
|
||||
|
||||
@@ -33,7 +33,12 @@ export interface LoaderProps extends SVGProps<SVGSVGElement> {
|
||||
variant?: "neutral" | "interaction" | "inverted";
|
||||
}
|
||||
|
||||
export const Loader = forwardRef<SVGSVGElement, LoaderProps>(
|
||||
/* Workaround for @types/react v17/v18 feil */
|
||||
export type LoaderType = React.ForwardRefExoticComponent<
|
||||
LoaderProps & React.RefAttributes<SVGSVGElement>
|
||||
>;
|
||||
|
||||
export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @navikt/ds-tailwind
|
||||
|
||||
## 2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
## 2.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
## 2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "Tailwind config based on @navikt/ds-tokens",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -22,7 +22,7 @@
|
||||
"directory": "@navikt/core/tailwind"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^2.0.18",
|
||||
"@navikt/ds-tokens": "^2.1.6",
|
||||
"@types/jest": "^29.0.0",
|
||||
"color": "4.2.3",
|
||||
"jest": "^29.0.0",
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @navikt/ds-tokens
|
||||
|
||||
## 2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
## 2.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
|
||||
:bug: Fikset feil danger-hover token
|
||||
|
||||
## 2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
{ "name": "--a-surface-danger-subtle", "value": "rgba(255, 230, 230, 1)" },
|
||||
{
|
||||
"name": "--a-surface-danger-subtle-hover",
|
||||
"value": "rgba(255, 230, 230, 1)"
|
||||
"value": "rgba(255, 184, 184, 1)"
|
||||
},
|
||||
{ "name": "--a-surface-danger", "value": "rgba(195, 0, 0, 1)" },
|
||||
{ "name": "--a-surface-danger-hover", "value": "rgba(173, 0, 0, 1)" },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "Design-tokens for NAV designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
"danger": {
|
||||
"subtle": {
|
||||
"hover": { "value": "{a.red.50.value}" },
|
||||
"hover": { "value": "{a.red.100.value}" },
|
||||
"@": { "value": "{a.red.50.value}" }
|
||||
},
|
||||
"hover": {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @navikt/ds-icons
|
||||
|
||||
## 2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1759](https://github.com/navikt/Designsystemet/pull/1759) [`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset @types/react v18 feil introdusert i v2.0.6
|
||||
|
||||
## 2.1.0
|
||||
|
||||
## 2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Fikser https://github.com/navikt/Designsystemet/issues/1758
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const files = fs.readdirSync(`./esm`).filter((x) => x.endsWith(".d.ts"));
|
||||
|
||||
files.forEach((file) => {
|
||||
let data = fs.readFileSync(`./esm/${file}`).toString().split("\n");
|
||||
|
||||
data = data.map((x) => {
|
||||
return x.includes("React.ForwardRefExoticComponent")
|
||||
? x.split(":")[0] +
|
||||
`: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & SVGRProps & React.RefAttributes<SVGSVGElement>>;`
|
||||
: x;
|
||||
});
|
||||
|
||||
fs.writeFileSync(`./esm/${file}`, data.join("\n"));
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-icons",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "Aksel icons for NAV",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"repository": {
|
||||
@@ -32,7 +32,7 @@
|
||||
"create-package-json-pointers-to-esm": "node ../../scripts/createPackageJsonsWithESMPointers.js",
|
||||
"create-icons": "svgr --silent --out-dir src svg",
|
||||
"clean": "rimraf src cjs esm",
|
||||
"build": "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\" && node cleanTypes.js && yarn create-package-json-pointers-to-esm",
|
||||
"update": "ts-node figma/index.ts dotenv/config",
|
||||
"test": "jest"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @navikt/ds-css-internal
|
||||
|
||||
## 2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
## 2.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
|
||||
:bug: Fikset feil danger-hover token
|
||||
|
||||
## 2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "copy-to-clipboard.css";
|
||||
@import "dropdown.css";
|
||||
@import "header.css";
|
||||
@import "timeline.css";
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css-internal",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "CSS for NAV internal Designsystem-components",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
.navdsi-timeline {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
width: 100%;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.navdsi-timeline__axislabels {
|
||||
position: relative;
|
||||
height: 1rem;
|
||||
box-sizing: content-box;
|
||||
grid-column: 2;
|
||||
margin-bottom: var(--a-spacing-4);
|
||||
margin-left: var(--a-spacing-8);
|
||||
}
|
||||
|
||||
.navdsi-timeline__row-label {
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
margin-right: var(--a-spacing-4);
|
||||
grid-column: 1;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navdsi-timeline__axislabels-label {
|
||||
position: absolute;
|
||||
color: var(--ac-timeline-axislabel-text, var(--a-text-subtle));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navdsi-timeline__row {
|
||||
display: flex;
|
||||
background: var(--ac-timeline-row-bg, var(--a-surface-subtle));
|
||||
margin: 0 0 var(--a-spacing-8);
|
||||
padding-left: var(--a-spacing-8);
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.navdsi-timeline__row--active {
|
||||
background: var(--ac-timeline-row-active-bg, var(--a-surface-selected));
|
||||
}
|
||||
|
||||
.navdsi-timeline__row:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navdsi-timeline__row-periods {
|
||||
height: 1.5rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navdsi-timeline__row-periods > li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.navdsi-timeline__period {
|
||||
height: 100%;
|
||||
border-radius: var(--a-border-radius-full);
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--inner {
|
||||
margin: 0 var(--a-spacing-1) 0 var(--a-spacing-2);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: clip;
|
||||
text-align: left;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* .navdsi-timeline__period > * {
|
||||
flex-shrink: 0;
|
||||
} */
|
||||
|
||||
.navdsi-timeline__period--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--success {
|
||||
background: var(--ac-timeline-period-success-bg, var(--a-surface-success-subtle));
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-success-border, var(--a-border-success));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--success.navdsi-timeline__period--clickable:hover {
|
||||
background: var(--a-surface-success-subtle-hover);
|
||||
background: var(--ac-timeline-period-success-bg-hover, var(--a-surface-success-subtle-hover));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--warning {
|
||||
background: var(--ac-timeline-period-warning-bg, var(--a-surface-warning-subtle));
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-warning-border, var(--a-border-warning));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--warning.navdsi-timeline__period--clickable:hover {
|
||||
background: var(--ac-timeline-period-warning-bg-hover, var(--a-surface-warning-subtle-hover));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--danger {
|
||||
background: var(--ac-timeline-period-danger-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-danger-border, var(--a-border-danger));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--danger.navdsi-timeline__period--clickable:hover {
|
||||
background: var(--ac-timeline-period-danger-bg-hover, var(--a-surface-danger-subtle-hover));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--info {
|
||||
background: var(--ac-timeline-period-info-bg, var(--a-surface-info-subtle));
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-info-border, var(--a-border-info));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--info.navdsi-timeline__period--clickable:hover {
|
||||
background: var(--ac-timeline-period-info-bg-hover, var(--a-surface-info-subtle-hover));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--neutral {
|
||||
background: var(--ac-timeline-period-neutral-bg, var(--a-bg-subtle));
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-neutral-border, var(--a-border-default));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--neutral.navdsi-timeline__period--clickable:hover {
|
||||
background: var(--ac-timeline-period-neutral-bg-hover, var(--a-gray-200));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--connected-both {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--connected-right {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--connected-left {
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--selected {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-timeline-period-selected-border, var(--a-border-action-selected));
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.navdsi-timeline__pin-wrapper {
|
||||
position: absolute;
|
||||
grid-column: 2;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
isolation: isolate;
|
||||
|
||||
--navdsc-timeline-pin-size: 0.9rem;
|
||||
}
|
||||
|
||||
.navdsi-timeline__pin-button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: var(--a-surface-default);
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg, var(--a-surface-danger)),
|
||||
0 0 0 4px var(--ac-timeline-pin-bg, var(--a-surface-danger)), 0 0 0 5px var(--a-surface-default);
|
||||
z-index: 2;
|
||||
border-radius: var(--a-border-radius-full);
|
||||
padding: 3.5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navdsi-timeline__pin-button::before {
|
||||
content: "";
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: var(--a-border-radius-full);
|
||||
left: calc(-12.5px + 3.5px);
|
||||
top: calc(-12.5px + 3.5px);
|
||||
}
|
||||
|
||||
.navdsi-timeline__pin-button:focus {
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg, var(--a-surface-danger)),
|
||||
0 0 0 3px var(--ac-timeline-pin-bg, var(--a-surface-danger)), 0 0 0 4px var(--a-surface-default),
|
||||
0 0 0 6px var(--a-border-focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.navdsi-timeline__pin-button:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)),
|
||||
0 0 0 4px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)), 0 0 0 5px var(--a-surface-default);
|
||||
}
|
||||
|
||||
.navdsi-timeline__pin-button:hover:focus {
|
||||
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)),
|
||||
0 0 0 3px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)), 0 0 0 4px var(--a-surface-default),
|
||||
0 0 0 6px var(--a-border-focus);
|
||||
}
|
||||
|
||||
.navdsi-timeline__pin-wrapper::before {
|
||||
content: "";
|
||||
top: var(--navdsc-timeline-pin-size);
|
||||
height: calc(94% - var(--navdsc-timeline-pin-size));
|
||||
width: 1px;
|
||||
margin: 0 auto;
|
||||
background: var(--a-surface-inverted);
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom {
|
||||
margin-top: var(--a-spacing-6);
|
||||
float: right;
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom-button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
padding: 6px 9px 6px 8px;
|
||||
border: 1px solid var(--ac-timeline-zoom-border, var(--a-border-default));
|
||||
border-width: 1px 0 1px 1px;
|
||||
background: var(--ac-timeline-zoom-bg, var(--a-surface-default));
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom li:first-child .navdsi-timeline__zoom-button {
|
||||
border-radius: var(--a-border-radius-medium) 0 0 var(--a-border-radius-medium);
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom li:last-child .navdsi-timeline__zoom-button {
|
||||
border-width: 1px;
|
||||
border-radius: 0 var(--a-border-radius-medium) var(--a-border-radius-medium) 0;
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom li:only-child .navdsi-timeline__zoom-button {
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom-button:not([aria-pressed="true"]):hover {
|
||||
background: var(--ac-timeline-zoom-hover, var(--a-surface-action-subtle-hover));
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom-button[aria-pressed="true"] {
|
||||
background: var(--ac-timeline-zoom-selected-bg, var(--a-surface-inverted));
|
||||
color: var(--ac-timeline-zoom-selected-text, var(--a-text-on-inverted));
|
||||
}
|
||||
|
||||
.navdsi-timeline__period:focus {
|
||||
outline: none;
|
||||
z-index: 4;
|
||||
box-shadow: 0 0 0 2px var(--a-border-focus);
|
||||
}
|
||||
|
||||
.navdsi-timeline__period--selected:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-timeline-period-selected-border, var(--a-border-action-selected)),
|
||||
0 0 0 2px var(--a-border-focus);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom li:focus-within {
|
||||
z-index: var(--a-z-index-focus);
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom-button:focus {
|
||||
box-shadow: 0 0 0 1px var(--a-surface-default), 0 0 0 3px var(--a-border-focus);
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.navdsi-timeline__zoom li:not(:last-child) .navdsi-timeline__zoom-button:focus {
|
||||
margin-right: 2px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
@@ -16,5 +16,33 @@
|
||||
"--ac-header-text": "--a-text-on-inverted",
|
||||
"--ac-header-hover-bg": "--a-surface-inverted-hover",
|
||||
"--ac-header-active-bg": "--a-surface-inverted-active"
|
||||
},
|
||||
"timeline": {
|
||||
"--ac-timeline-row-bg": "--a-surface-subtle",
|
||||
"--ac-timeline-row-active-bg": "--a-surface-selected",
|
||||
"--ac-timeline-period-success-bg": "--a-surface-success-subtle",
|
||||
"--ac-timeline-period-success-border": "--a-border-success",
|
||||
"--ac-timeline-period-success-bg-hover": "--a-surface-success-subtle-hover",
|
||||
"--ac-timeline-period-warning-bg": "--a-surface-warning-subtle",
|
||||
"--ac-timeline-period-warning-border": "--a-border-warning",
|
||||
"--ac-timeline-period-warning-bg-hover": "--a-surface-warning-subtle-hover",
|
||||
"--ac-timeline-period-danger-bg": "--a-surface-danger-subtle",
|
||||
"--ac-timeline-period-danger-border": "--a-border-danger",
|
||||
"--ac-timeline-period-danger-bg-hover": "--a-surface-danger-subtle-hover",
|
||||
"--ac-timeline-period-info-bg": "--a-surface-info-subtle",
|
||||
"--ac-timeline-period-info-border": "--a-border-info",
|
||||
"--ac-timeline-period-info-bg-hover": "--a-surface-info-subtle-hover",
|
||||
"--ac-timeline-period-neutral-bg": "--a-bg-subtle",
|
||||
"--ac-timeline-period-neutral-border": "--a-border-default",
|
||||
"--ac-timeline-period-neutral-bg-hover": "--a-gray-200",
|
||||
"--ac-timeline-period-selected-border": "--a-border-action-selected",
|
||||
"--ac-timeline-pin-bg": "--a-surface-danger",
|
||||
"--ac-timeline-pin-bg-hover": "--a-surface-danger-hover",
|
||||
"--ac-timeline-zoom-border": "--a-border-default",
|
||||
"--ac-timeline-zoom-bg": "--a-surface-default",
|
||||
"--ac-timeline-zoom-hover": "--a-surface-action-subtle-hover",
|
||||
"--ac-timeline-zoom-selected-bg": "--a-surface-inverted",
|
||||
"--ac-timeline-zoom-selected-text": "--a-text-on-inverted",
|
||||
"--ac-timeline-axislabel-text": "--a-text-subtle"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,66 @@
|
||||
# @navikt/ds-react-internal
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`cd1c93f9d`](https://github.com/navikt/Designsystemet/commit/cd1c93f9d471fbbc38215d788a73f21b5b216c5b)]:
|
||||
- @navikt/ds-react@2.1.6
|
||||
- @navikt/ds-icons@2.1.6
|
||||
|
||||
## 2.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-react@2.1.5
|
||||
- @navikt/ds-icons@2.1.5
|
||||
|
||||
## 2.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`87f194c1d`](https://github.com/navikt/Designsystemet/commit/87f194c1dc29b10a604de676b358d117e2dbf52f)]:
|
||||
- @navikt/ds-react@2.1.4
|
||||
- @navikt/ds-icons@2.1.4
|
||||
|
||||
## 2.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`494566e60`](https://github.com/navikt/Designsystemet/commit/494566e604e958363f75ebfa2adbc1669397636a)]:
|
||||
- @navikt/ds-react@2.1.3
|
||||
- @navikt/ds-icons@2.1.3
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-react@2.1.2
|
||||
- @navikt/ds-icons@2.1.2
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57)]:
|
||||
- @navikt/ds-react@2.1.1
|
||||
- @navikt/ds-icons@2.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
|
||||
:bug: Fikset feil danger-hover token
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`0873fe652`](https://github.com/navikt/Designsystemet/commit/0873fe6520e817e462197c162b49c05826da4838)]:
|
||||
- @navikt/ds-react@2.1.0
|
||||
- @navikt/ds-icons@2.1.0
|
||||
|
||||
## 2.0.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react-internal",
|
||||
"version": "2.0.18",
|
||||
"version": "2.1.6",
|
||||
"description": "Aksel react components for internal UIs in NAV",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -35,8 +35,8 @@
|
||||
"watch": "tsc --watch -p tsconfig.esm.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@navikt/ds-icons": "^2.0.18",
|
||||
"@navikt/ds-react": "^2.0.18",
|
||||
"@navikt/ds-icons": "^2.1.6",
|
||||
"@navikt/ds-react": "^2.1.6",
|
||||
"clsx": "^1.1.1",
|
||||
"copy-to-clipboard": "^3.3.1"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./copy-to-clipboard";
|
||||
export * from "./dropdown";
|
||||
export * from "./header";
|
||||
export * from "./timeline";
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Detail } from "@navikt/ds-react";
|
||||
import {
|
||||
addDays,
|
||||
addMonths,
|
||||
addYears,
|
||||
differenceInDays,
|
||||
differenceInMonths,
|
||||
differenceInYears,
|
||||
endOfMonth,
|
||||
endOfYear,
|
||||
format,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import React from "react";
|
||||
import { useTimelineContext } from "./hooks/useTimelineContext";
|
||||
import { isVisible } from "./utils";
|
||||
import { horizontalPositionAndWidth } from "./utils/calc";
|
||||
import { AxisLabel } from "./utils/types.external";
|
||||
|
||||
export const dayLabels = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
totalDays: number,
|
||||
direction: "left" | "right"
|
||||
): AxisLabel[] => {
|
||||
const increment = Math.ceil(totalDays / 10);
|
||||
const lastDay = startOfDay(end);
|
||||
return new Array(totalDays)
|
||||
.fill(lastDay)
|
||||
.map((thisDay, i) => {
|
||||
if (i % increment !== 0) return null;
|
||||
const day: Date = subDays(thisDay, i);
|
||||
const { horizontalPosition, width } = horizontalPositionAndWidth(
|
||||
day,
|
||||
addDays(day, 1),
|
||||
start,
|
||||
end
|
||||
);
|
||||
return {
|
||||
direction: direction,
|
||||
horizontalPosition: horizontalPosition,
|
||||
label: format(day, "MM.dd"),
|
||||
date: day,
|
||||
width: width,
|
||||
};
|
||||
})
|
||||
.filter((label) => label !== null) as AxisLabel[];
|
||||
};
|
||||
|
||||
export const monthLabels = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
direction: "left" | "right"
|
||||
): AxisLabel[] => {
|
||||
const startMonth = startOfMonth(start);
|
||||
const endMonth = endOfMonth(end);
|
||||
const numberOfMonths = differenceInMonths(endMonth, startMonth) + 1;
|
||||
return new Array(numberOfMonths).fill(startMonth).map((thisMonth, i) => {
|
||||
const month: Date = addMonths(thisMonth, i);
|
||||
const { horizontalPosition, width } = horizontalPositionAndWidth(
|
||||
month,
|
||||
addMonths(month, 1),
|
||||
start,
|
||||
end
|
||||
);
|
||||
return {
|
||||
direction: direction,
|
||||
horizontalPosition: horizontalPosition,
|
||||
label: format(month, "MMM yy"),
|
||||
date: month,
|
||||
width: width,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const yearLabels = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
direction: "left" | "right"
|
||||
): AxisLabel[] => {
|
||||
const firstYear = startOfYear(start);
|
||||
const lastYear = endOfYear(end);
|
||||
const yearCount = differenceInYears(lastYear, start) + 1;
|
||||
return new Array(yearCount).fill(firstYear).map((thisYear, i) => {
|
||||
const year: Date = addYears(thisYear, i);
|
||||
const { horizontalPosition, width } = horizontalPositionAndWidth(
|
||||
year,
|
||||
addYears(year, 1),
|
||||
start,
|
||||
end
|
||||
);
|
||||
return {
|
||||
direction: direction,
|
||||
horizontalPosition: horizontalPosition,
|
||||
label: year.getFullYear().toString(),
|
||||
date: year,
|
||||
width: width,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const axisLabels = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
direction: "left" | "right"
|
||||
): AxisLabel[] => {
|
||||
const totalDays = differenceInDays(end, start);
|
||||
if (totalDays < 40) {
|
||||
return dayLabels(start, end, totalDays, direction);
|
||||
} else if (totalDays < 370) {
|
||||
return monthLabels(start, end, direction);
|
||||
} else {
|
||||
return yearLabels(start, end, direction);
|
||||
}
|
||||
};
|
||||
|
||||
export const AxisLabels = () => {
|
||||
const { endDate, startDate, direction } = useTimelineContext();
|
||||
const labels = axisLabels(startDate, endDate, direction).filter(isVisible);
|
||||
|
||||
return (
|
||||
<div className="navdsi-timeline__axislabels" aria-hidden="true">
|
||||
{labels.map((etikett) => (
|
||||
<Detail
|
||||
className="navdsi-timeline__axislabels-label"
|
||||
as="div"
|
||||
key={etikett.label}
|
||||
style={{
|
||||
justifyContent: direction === "left" ? "flex-start" : "flex-end",
|
||||
[direction]: `${etikett.horizontalPosition}%`,
|
||||
width: `${etikett.width}%`,
|
||||
}}
|
||||
>
|
||||
{etikett.label}
|
||||
</Detail>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { mergeRefs, Popover } from "@navikt/ds-react";
|
||||
import { format } from "date-fns";
|
||||
import React, { forwardRef, useMemo, useRef, useState } from "react";
|
||||
import { useTimelineContext } from "./hooks/useTimelineContext";
|
||||
import { position } from "./utils/calc";
|
||||
import { TimelineComponentTypes } from "./utils/types.internal";
|
||||
|
||||
export interface TimelinePinProps {
|
||||
/**
|
||||
* Date position for the pin.
|
||||
*/
|
||||
date: Date;
|
||||
/**
|
||||
* Content in Pin Popover.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface PinType
|
||||
extends React.ForwardRefExoticComponent<
|
||||
TimelinePinProps & React.RefAttributes<HTMLButtonElement>
|
||||
> {
|
||||
componentType: TimelineComponentTypes;
|
||||
}
|
||||
|
||||
export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
|
||||
({ date, children, ...rest }, ref) => {
|
||||
const { startDate, endDate, direction } = useTimelineContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const mergedRef = useMemo(() => mergeRefs([buttonRef, ref]), [ref]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="navdsi-timeline__pin-wrapper"
|
||||
style={{ [direction]: `${position(date, startDate, endDate)}%` }}
|
||||
>
|
||||
<button
|
||||
{...rest}
|
||||
ref={mergedRef}
|
||||
className="navdsi-timeline__pin-button"
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-label={`pin:${format(date, "dd.MM.yyyy")}`}
|
||||
aria-expanded={open}
|
||||
/>
|
||||
</div>
|
||||
{children && (
|
||||
<Popover
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchorEl={buttonRef.current}
|
||||
offset={18}
|
||||
>
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
) as PinType;
|
||||
|
||||
Pin.componentType = "pin";
|
||||
|
||||
export default Pin;
|
||||
@@ -0,0 +1,209 @@
|
||||
import { endOfDay, isSameDay, startOfDay } from "date-fns";
|
||||
import React, { forwardRef, useMemo, useRef, useState } from "react";
|
||||
import { AxisLabels } from "./AxisLabels";
|
||||
import { RowContext } from "./hooks/useRowContext";
|
||||
import { TimelineContext } from "./hooks/useTimelineContext";
|
||||
import {
|
||||
useEarliestDate,
|
||||
useLatestDate,
|
||||
useTimelineRows,
|
||||
} from "./hooks/useTimelineRows";
|
||||
import Period, { PeriodType } from "./period";
|
||||
import Pin, { PinType } from "./Pin";
|
||||
import TimelineRow, { TimelineRowType } from "./TimelineRow";
|
||||
import { parseRows } from "./utils/timeline";
|
||||
import Zoom, { ZoomType } from "./zoom";
|
||||
|
||||
export interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Decides startingpoint in timeline.
|
||||
* Defaults to earliest date among the timeline periods.
|
||||
*/
|
||||
startDate?: Date;
|
||||
/**
|
||||
* Decides end-date for timeline.
|
||||
* Defaults to the latest date among the timeline periods.
|
||||
*/
|
||||
endDate?: Date;
|
||||
/**
|
||||
* Decides direction which periods are sorted/displayed. "left" ascends from earliest date on left.
|
||||
* @default "left"
|
||||
*/
|
||||
direction?: "left" | "right";
|
||||
}
|
||||
|
||||
interface TimelineComponent
|
||||
extends React.ForwardRefExoticComponent<TimelineProps> {
|
||||
/**
|
||||
* Built-in timeline row
|
||||
*/
|
||||
Row: TimelineRowType;
|
||||
/**
|
||||
* Built-in row period
|
||||
*/
|
||||
Period: PeriodType;
|
||||
/**
|
||||
* Built-in timeline pin
|
||||
*/
|
||||
Pin: PinType;
|
||||
/**
|
||||
* Built-in timeline zoom-component
|
||||
*/
|
||||
Zoom: ZoomType;
|
||||
}
|
||||
|
||||
export const Timeline = forwardRef<HTMLDivElement, TimelineProps>(
|
||||
({ children, startDate, endDate, direction = "left", ...rest }, ref) => {
|
||||
const isMultipleRows = Array.isArray(children);
|
||||
|
||||
const firstFocusabled = useRef<
|
||||
{ ref: HTMLButtonElement | null; id: number }[]
|
||||
>([]);
|
||||
|
||||
if (!isMultipleRows) {
|
||||
children = [children];
|
||||
}
|
||||
const rowChildren = React.Children.toArray(children).filter(
|
||||
(c: any) => c?.type?.componentType === "row"
|
||||
);
|
||||
|
||||
const pins = React.Children.toArray(children).filter(
|
||||
(c: any) => c?.type?.componentType === "pin"
|
||||
);
|
||||
|
||||
const zoomComponent = React.Children.toArray(children).find(
|
||||
(c: any) => c?.type?.componentType === "zoom"
|
||||
);
|
||||
|
||||
const rowsRaw = useMemo(() => {
|
||||
return parseRows(rowChildren);
|
||||
}, [rowChildren]);
|
||||
|
||||
const rows = rowsRaw.map((r) => {
|
||||
if (r?.periods) {
|
||||
return r.periods;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const initialStartDate = startOfDay(useEarliestDate({ startDate, rows }));
|
||||
const [start, setStart] = useState(initialStartDate);
|
||||
const [activeRow, setActiveRow] = useState<number | null>(null);
|
||||
const [endInclusive, setEndInclusive] = useState(
|
||||
endOfDay(useLatestDate({ endDate, rows }))
|
||||
);
|
||||
|
||||
const initialEndDate = endOfDay(useLatestDate({ endDate, rows }));
|
||||
const processedRows = useTimelineRows(
|
||||
rowsRaw,
|
||||
start,
|
||||
endInclusive,
|
||||
direction
|
||||
);
|
||||
|
||||
const handleZoomChange = (zoomStart: Date) => {
|
||||
if (direction === "left") {
|
||||
if (isSameDay(zoomStart, start)) {
|
||||
setStart(initialStartDate);
|
||||
return;
|
||||
}
|
||||
setStart(zoomStart);
|
||||
} else {
|
||||
if (isSameDay(zoomStart, endInclusive)) {
|
||||
setEndInclusive(initialEndDate);
|
||||
return;
|
||||
}
|
||||
setEndInclusive(zoomStart);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveRowChange = (key: string) => {
|
||||
if (activeRow !== null && key === "ArrowDown") {
|
||||
for (let i = activeRow + 1; i < processedRows.length; i++) {
|
||||
const row = processedRows[i];
|
||||
if (row.periods.find((p) => !!p.children || !!p.onSelectPeriod)) {
|
||||
setActiveRow(i);
|
||||
firstFocusabled.current.find((x) => x.id === i)?.ref?.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (activeRow !== null && key === "ArrowUp") {
|
||||
for (let i = activeRow - 1; i >= 0; i--) {
|
||||
const row = processedRows[i];
|
||||
if (row.periods.find((p) => !!p.children || !!p.onSelectPeriod)) {
|
||||
setActiveRow(i);
|
||||
firstFocusabled.current.find((x) => x.id === i)?.ref?.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const addFocusable = (ref: HTMLButtonElement | null, id: number) => {
|
||||
let items = firstFocusabled.current;
|
||||
items = items.filter((x) => x.id !== id);
|
||||
items.push({ ref, id });
|
||||
firstFocusabled.current = items;
|
||||
};
|
||||
|
||||
return (
|
||||
<TimelineContext.Provider
|
||||
value={{
|
||||
startDate: start,
|
||||
endDate: endInclusive,
|
||||
direction: direction,
|
||||
setStart: (d) => handleZoomChange(d),
|
||||
setEndInclusive: (d) => setEndInclusive(d),
|
||||
activeRow: activeRow,
|
||||
setActiveRow: (key) => handleActiveRowChange(key),
|
||||
initiate: (i) => setActiveRow(i),
|
||||
addFocusable,
|
||||
}}
|
||||
>
|
||||
<div {...rest} ref={ref}>
|
||||
<div className="navdsi-timeline">
|
||||
<AxisLabels />
|
||||
|
||||
{pins.map((pin) => {
|
||||
return pin;
|
||||
})}
|
||||
|
||||
{processedRows.map((row, i) => {
|
||||
return (
|
||||
<RowContext.Provider
|
||||
key={`row-${row.id}`}
|
||||
value={{
|
||||
periods: row.periods,
|
||||
id: row.id,
|
||||
active: activeRow === i,
|
||||
index: i,
|
||||
}}
|
||||
>
|
||||
<TimelineRow
|
||||
{...row?.restProps}
|
||||
ref={row?.ref}
|
||||
label={row.label}
|
||||
icon={row.icon}
|
||||
headingTag={row.headingTag}
|
||||
/>
|
||||
</RowContext.Provider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{zoomComponent && zoomComponent}
|
||||
</div>
|
||||
</TimelineContext.Provider>
|
||||
);
|
||||
}
|
||||
) as TimelineComponent;
|
||||
|
||||
Timeline.Row = TimelineRow;
|
||||
Timeline.Period = Period;
|
||||
Timeline.Pin = Pin;
|
||||
Timeline.Zoom = Zoom;
|
||||
|
||||
export default Timeline;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { BodyShort } from "@navikt/ds-react";
|
||||
import { format } from "date-fns";
|
||||
import React, { forwardRef } from "react";
|
||||
import { PeriodContext } from "./hooks/usePeriodContext";
|
||||
import { useRowContext } from "./hooks/useRowContext";
|
||||
import { useTimelineContext } from "./hooks/useTimelineContext";
|
||||
import Period from "./period";
|
||||
import {
|
||||
PositionedPeriod,
|
||||
TimelineComponentTypes,
|
||||
} from "./utils/types.internal";
|
||||
import cl from "clsx";
|
||||
|
||||
export interface TimelineRowProps
|
||||
extends React.HTMLAttributes<HTMLOListElement> {
|
||||
/**
|
||||
* Label for the timeline row
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Heading level for the label e.g h2, h3...
|
||||
* @default "h3"
|
||||
*/
|
||||
headingTag?: "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
/**
|
||||
* Icon next to label
|
||||
*/
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface TimelineRowType
|
||||
extends React.ForwardRefExoticComponent<
|
||||
TimelineRowProps & React.RefAttributes<HTMLOListElement>
|
||||
> {
|
||||
componentType: TimelineComponentTypes;
|
||||
}
|
||||
|
||||
export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
|
||||
({ label, headingTag = "h3", icon, ...rest }, ref) => {
|
||||
const { periods, id, active } = useRowContext();
|
||||
const { setActiveRow } = useTimelineContext();
|
||||
|
||||
const latest = periods.reduce((a, b) => {
|
||||
return a.end > b.end ? a : b;
|
||||
}, {} as PositionedPeriod);
|
||||
|
||||
const earliest = periods.reduce((a, b) => {
|
||||
return a.end < b.end ? a : b;
|
||||
}, {} as PositionedPeriod);
|
||||
|
||||
const firstFocusable = periods.find(
|
||||
(p) => !!p.children || !!p.onSelectPeriod
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{label && (
|
||||
<BodyShort
|
||||
as={headingTag}
|
||||
id={`timeline-row-${id}`}
|
||||
className="navdsi-timeline__row-label"
|
||||
size="small"
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</BodyShort>
|
||||
)}
|
||||
<div
|
||||
className={cl("navdsi-timeline__row", {
|
||||
"navdsi-timeline__row--active": active,
|
||||
})}
|
||||
>
|
||||
<ol
|
||||
{...rest}
|
||||
ref={ref}
|
||||
aria-label={
|
||||
periods.length === 0
|
||||
? "Ingen perioder"
|
||||
: `${format(earliest.start, "dd.MM.yyyy")} til ${format(
|
||||
latest.end,
|
||||
"dd.MM.yyyy"
|
||||
)}`
|
||||
}
|
||||
className="navdsi-timeline__row-periods"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setActiveRow(e.key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{periods &&
|
||||
periods.map((period) => {
|
||||
return (
|
||||
<li key={`period-${period.id}`}>
|
||||
<PeriodContext.Provider
|
||||
value={{
|
||||
periodId: period.id,
|
||||
firstFocus: firstFocusable?.id === period.id,
|
||||
restProps: period?.restProps,
|
||||
}}
|
||||
>
|
||||
<Period
|
||||
start={period.start}
|
||||
end={period.endInclusive}
|
||||
icon={period.icon}
|
||||
ref={period?.ref}
|
||||
/>
|
||||
</PeriodContext.Provider>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
) as TimelineRowType;
|
||||
|
||||
TimelineRow.componentType = "row";
|
||||
|
||||
export default TimelineRow;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface PeriodContextProps {
|
||||
periodId: String;
|
||||
firstFocus: boolean;
|
||||
restProps?: any;
|
||||
}
|
||||
|
||||
export const PeriodContext = createContext<PeriodContextProps>({
|
||||
periodId: "",
|
||||
firstFocus: false,
|
||||
});
|
||||
|
||||
export const usePeriodContext = () => {
|
||||
const context = useContext(PeriodContext);
|
||||
|
||||
if (!context) {
|
||||
console.warn("usePeriodContext must be used with PeriodContext");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { PositionedPeriod } from "../utils/types.internal";
|
||||
|
||||
interface RowContextProps {
|
||||
periods: PositionedPeriod[];
|
||||
id: string;
|
||||
active: boolean;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const RowContext = createContext<RowContextProps>({
|
||||
periods: [],
|
||||
id: "",
|
||||
active: false,
|
||||
index: 0,
|
||||
});
|
||||
|
||||
export const useRowContext = () => {
|
||||
const context = useContext(RowContext);
|
||||
|
||||
if (!context) {
|
||||
console.warn("useRowContext must be used with RowContext");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface TimelineContextProps {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
direction: "left" | "right";
|
||||
setStart: (d: Date) => void;
|
||||
setEndInclusive: (d: Date) => void;
|
||||
activeRow: number | null;
|
||||
setActiveRow: (i: string) => void;
|
||||
initiate: (i: number) => void;
|
||||
addFocusable: (ref: HTMLButtonElement | null, id: number) => void;
|
||||
}
|
||||
|
||||
export const TimelineContext = createContext<TimelineContextProps>({
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
direction: "left",
|
||||
setStart: () => null,
|
||||
setEndInclusive: () => null,
|
||||
activeRow: 0,
|
||||
setActiveRow: () => null,
|
||||
initiate: () => null,
|
||||
addFocusable: () => null,
|
||||
});
|
||||
|
||||
export const useTimelineContext = () => {
|
||||
const context = useContext(TimelineContext);
|
||||
|
||||
if (!context) {
|
||||
console.warn(
|
||||
"useTimelineContext must be used with TimelineContext (<Timeline />)"
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { addDays, endOfDay, isAfter, startOfDay, subDays } from "date-fns";
|
||||
|
||||
import { lastPeriod } from "../utils/sort";
|
||||
import { Period } from "../utils/types.external";
|
||||
import {
|
||||
InternalSimpleTimeline,
|
||||
PositionedPeriod,
|
||||
} from "../utils/types.internal";
|
||||
|
||||
import { horizontalPositionAndWidth } from "../utils/calc";
|
||||
import { invisiblePeriods, withinADay } from "../utils/filter";
|
||||
|
||||
const spatialPeriod = (
|
||||
period: Period,
|
||||
timelineStart: Date,
|
||||
timelineEndInclusive: Date,
|
||||
direction: "left" | "right" = "left",
|
||||
i: number,
|
||||
periods: PositionedPeriod[],
|
||||
rowIndex: number
|
||||
): PositionedPeriod => {
|
||||
const start = period.start;
|
||||
const endInclusive = period.end;
|
||||
|
||||
const rightOverlap =
|
||||
i < periods.length - 1 && !isAfter(periods[i + 1].start, endInclusive);
|
||||
|
||||
const { horizontalPosition, width } = horizontalPositionAndWidth(
|
||||
startOfDay(start),
|
||||
endOfDay(
|
||||
rightOverlap ? startOfDay(subDays(periods[i + 1].start, 1)) : endInclusive
|
||||
),
|
||||
timelineStart,
|
||||
timelineEndInclusive
|
||||
);
|
||||
|
||||
return {
|
||||
id: `r-${rowIndex}-p-${i}`,
|
||||
start: start,
|
||||
endInclusive: endInclusive,
|
||||
horizontalPosition: horizontalPosition,
|
||||
direction: direction,
|
||||
width: width,
|
||||
end: endInclusive,
|
||||
status: period.status,
|
||||
onSelectPeriod: period.onSelectPeriod,
|
||||
icon: period.icon,
|
||||
children: period.children,
|
||||
isActive: period.isActive,
|
||||
statusLabel: period.statusLabel,
|
||||
};
|
||||
};
|
||||
|
||||
const adjustedEdges = (
|
||||
period: PositionedPeriod,
|
||||
i: number,
|
||||
allPeriods: PositionedPeriod[]
|
||||
): PositionedPeriod => {
|
||||
const left =
|
||||
i > 0 && withinADay(period.start, allPeriods[i - 1].endInclusive);
|
||||
const right =
|
||||
i < allPeriods.length - 1 &&
|
||||
withinADay(allPeriods[i + 1].start, period.endInclusive);
|
||||
|
||||
return left && right
|
||||
? { ...period, cropped: "both" }
|
||||
: left
|
||||
? { ...period, cropped: "left" }
|
||||
: right
|
||||
? { ...period, cropped: "right" }
|
||||
: period;
|
||||
};
|
||||
|
||||
const trimmedPeriods = (period: PositionedPeriod) => {
|
||||
let { horizontalPosition, width, cropped } = period;
|
||||
if (horizontalPosition + width > 100) {
|
||||
width = 100 - horizontalPosition;
|
||||
cropped = cropped === "left" || cropped === "both" ? "both" : "right";
|
||||
}
|
||||
if (horizontalPosition < 0 && horizontalPosition + width > 0) {
|
||||
width = horizontalPosition + width;
|
||||
horizontalPosition = 0;
|
||||
cropped = cropped === "right" || cropped === "both" ? "both" : "left";
|
||||
}
|
||||
|
||||
return {
|
||||
...period,
|
||||
width: width,
|
||||
horizontalPosition: horizontalPosition,
|
||||
cropped: cropped,
|
||||
};
|
||||
};
|
||||
|
||||
export const useTimelineRows = (
|
||||
rows: any,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
direction: "left" | "right"
|
||||
): InternalSimpleTimeline[] =>
|
||||
useMemo(
|
||||
() =>
|
||||
rows.map((periods: InternalSimpleTimeline, i: number) => {
|
||||
const rowIndex = i;
|
||||
const timelinePeriods = periods.periods
|
||||
.sort((a: Period, b: Period) => a.start.valueOf() - b.start.valueOf())
|
||||
.map((period: Period & { restProps?: any; ref?: any }, i) => ({
|
||||
...spatialPeriod(
|
||||
period,
|
||||
startDate,
|
||||
endDate,
|
||||
direction,
|
||||
i,
|
||||
periods.periods,
|
||||
rowIndex
|
||||
),
|
||||
restProps: period?.restProps,
|
||||
ref: period?.ref,
|
||||
}))
|
||||
.sort(lastPeriod)
|
||||
.map(adjustedEdges)
|
||||
.map(trimmedPeriods)
|
||||
.filter(invisiblePeriods);
|
||||
return {
|
||||
id: `tl-row-${rowIndex}`,
|
||||
label: periods.label,
|
||||
headingTag: periods.headingTag || "h3",
|
||||
icon: periods.icon,
|
||||
periods:
|
||||
direction === "left" ? timelinePeriods : timelinePeriods.reverse(),
|
||||
restProps: periods?.restProps,
|
||||
ref: periods?.ref,
|
||||
};
|
||||
}),
|
||||
[rows, startDate, endDate, direction]
|
||||
);
|
||||
|
||||
const earliestDate = (earliest: Date, period: Period) =>
|
||||
period.start < earliest ? period.start : earliest;
|
||||
|
||||
const earliestFomDate = (rader: Period[][]) =>
|
||||
rader.flat().reduce(earliestDate, new Date());
|
||||
|
||||
export const useEarliestDate = ({ startDate, rows }: any) =>
|
||||
useMemo(
|
||||
() => (startDate ? startDate : earliestFomDate(rows)),
|
||||
[startDate, rows]
|
||||
);
|
||||
|
||||
const latestDate = (latest: Date, period: Period) =>
|
||||
period.end > latest ? period.end : latest;
|
||||
|
||||
const latestTomDate = (rows: Period[][]) =>
|
||||
rows.flat().reduce(latestDate, new Date(0));
|
||||
|
||||
export const useLatestDate = ({ endDate, rows }: any) =>
|
||||
useMemo(
|
||||
() => (endDate ? endDate : addDays(latestTomDate(rows), 1)),
|
||||
[endDate, rows]
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
export { default as Timeline } from "./Timeline";
|
||||
export type { TimelineProps } from "./Timeline";
|
||||
export type { TimelineRowProps } from "./TimelineRow";
|
||||
export type { TimelinePinProps } from "./Pin";
|
||||
export type { TimelinePeriodProps } from "./period";
|
||||
export type { TimelineZoomButtonProps } from "./zoom/ZoomButton";
|
||||
@@ -0,0 +1,91 @@
|
||||
import { mergeRefs, Popover } from "@navikt/ds-react";
|
||||
import cl from "clsx";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { usePeriodContext } from "../hooks/usePeriodContext";
|
||||
import { useRowContext } from "../hooks/useRowContext";
|
||||
import { useTimelineContext } from "../hooks/useTimelineContext";
|
||||
import { ariaLabel, getConditionalClasses } from "../utils/period";
|
||||
import { PeriodProps } from "./index";
|
||||
|
||||
interface TimelineClickablePeriodProps extends PeriodProps {
|
||||
onSelectPeriod?: () => void;
|
||||
isActive?: boolean;
|
||||
periodRef: React.ForwardedRef<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const ClickablePeriod = React.memo(
|
||||
({
|
||||
onSelectPeriod,
|
||||
start,
|
||||
end,
|
||||
status,
|
||||
cropped,
|
||||
direction,
|
||||
left,
|
||||
width,
|
||||
icon,
|
||||
children,
|
||||
isActive,
|
||||
statusLabel,
|
||||
restProps,
|
||||
periodRef,
|
||||
}: TimelineClickablePeriodProps) => {
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [selected, setSelected] = useState(false);
|
||||
const { index } = useRowContext();
|
||||
const { firstFocus } = usePeriodContext();
|
||||
const { initiate, addFocusable } = useTimelineContext();
|
||||
|
||||
const mergedRef = useMemo(
|
||||
() => mergeRefs([buttonRef, periodRef]),
|
||||
[periodRef]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...restProps}
|
||||
ref={(r) => {
|
||||
firstFocus && addFocusable(r, index);
|
||||
mergedRef(r);
|
||||
}}
|
||||
onClick={() => {
|
||||
children && setSelected((x) => !x);
|
||||
onSelectPeriod?.();
|
||||
}}
|
||||
aria-label={ariaLabel(start, end, status, statusLabel)}
|
||||
className={cl(
|
||||
"navdsi-timeline__period--clickable",
|
||||
getConditionalClasses(cropped, direction, status),
|
||||
restProps?.className,
|
||||
{
|
||||
"navdsi-timeline__period--selected": isActive,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
[direction]: `${left}%`,
|
||||
}}
|
||||
aria-expanded={children ? selected : undefined}
|
||||
onFocus={() => {
|
||||
initiate(index);
|
||||
}}
|
||||
>
|
||||
<div className="navdsi-timeline__period--inner">{icon}</div>
|
||||
</button>
|
||||
{children && (
|
||||
<Popover
|
||||
open={selected}
|
||||
onClose={() => setSelected(false)}
|
||||
anchorEl={buttonRef.current}
|
||||
strategy="fixed"
|
||||
>
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ClickablePeriod;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { ariaLabel, getConditionalClasses } from "../utils/period";
|
||||
import { PeriodProps } from "./index";
|
||||
import cl from "clsx";
|
||||
|
||||
interface TimelineNonClickablePeriodProps extends PeriodProps {
|
||||
periodRef?: React.ForwardedRef<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const NonClickablePeriod = ({
|
||||
start,
|
||||
end,
|
||||
status,
|
||||
cropped,
|
||||
direction,
|
||||
left,
|
||||
width,
|
||||
icon,
|
||||
statusLabel,
|
||||
restProps,
|
||||
periodRef,
|
||||
}: TimelineNonClickablePeriodProps) => {
|
||||
return (
|
||||
<div
|
||||
ref={periodRef}
|
||||
{...restProps}
|
||||
className={cl(
|
||||
getConditionalClasses(cropped, direction, status),
|
||||
restProps?.classname
|
||||
)}
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
[direction]: `${left}%`,
|
||||
}}
|
||||
>
|
||||
<div className="navdsi-timeline__period--inner">
|
||||
{icon}
|
||||
<p className="sr-only">{ariaLabel(start, end, status, statusLabel)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NonClickablePeriod;
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { usePeriodContext } from "../hooks/usePeriodContext";
|
||||
import { useRowContext } from "../hooks/useRowContext";
|
||||
import { TimelineComponentTypes } from "../utils/types.internal";
|
||||
import ClickablePeriod from "./ClickablePeriod";
|
||||
import NonClickablePeriod from "./NonClickablePeriod";
|
||||
|
||||
export interface TimelinePeriodProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Period start date.
|
||||
*/
|
||||
start: Date;
|
||||
/**
|
||||
* Period end date.
|
||||
*/
|
||||
end: Date;
|
||||
/**
|
||||
* Icon for easier visual identification.
|
||||
*/
|
||||
icon?: React.ReactNode;
|
||||
/**
|
||||
* Period status.
|
||||
* @default "neutral"
|
||||
*/
|
||||
status?: "success" | "warning" | "danger" | "info" | "neutral";
|
||||
/**
|
||||
* Status label for screen-readers
|
||||
* e.g "Sykemeldt", "foreldrepermisjon"
|
||||
*/
|
||||
statusLabel?: string;
|
||||
/**
|
||||
* Callback when selecting a period.
|
||||
*/
|
||||
onSelectPeriod?: () => void;
|
||||
/**
|
||||
* Content displayed in Popover on click.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Visual active inidcation on period
|
||||
* @note Make sure only one period is active at a time
|
||||
*/
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface PeriodProps {
|
||||
start: Date;
|
||||
end: Date;
|
||||
status: string;
|
||||
cropped: string;
|
||||
direction: string;
|
||||
width: Number;
|
||||
left: Number;
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
statusLabel?: string;
|
||||
restProps?: any;
|
||||
}
|
||||
|
||||
export interface PeriodType
|
||||
extends React.ForwardRefExoticComponent<
|
||||
TimelinePeriodProps &
|
||||
React.RefAttributes<HTMLDivElement | HTMLButtonElement>
|
||||
> {
|
||||
componentType: TimelineComponentTypes;
|
||||
}
|
||||
|
||||
export const Period = forwardRef<HTMLDivElement, TimelinePeriodProps>(
|
||||
({ icon }, ref) => {
|
||||
const { periods } = useRowContext();
|
||||
const { periodId, restProps } = usePeriodContext();
|
||||
|
||||
const period = periods.find((p) => p.id === periodId);
|
||||
|
||||
if (!period) {
|
||||
return <></>;
|
||||
}
|
||||
const {
|
||||
start,
|
||||
endInclusive,
|
||||
width,
|
||||
horizontalPosition,
|
||||
status = "neutral",
|
||||
onSelectPeriod,
|
||||
cropped,
|
||||
direction,
|
||||
children,
|
||||
isActive,
|
||||
statusLabel,
|
||||
} = period;
|
||||
|
||||
return onSelectPeriod || children ? (
|
||||
<ClickablePeriod
|
||||
periodRef={ref as React.ForwardedRef<HTMLButtonElement>}
|
||||
start={start}
|
||||
end={endInclusive}
|
||||
status={status}
|
||||
onSelectPeriod={onSelectPeriod}
|
||||
cropped={cropped || ""}
|
||||
direction={direction}
|
||||
width={width}
|
||||
left={horizontalPosition}
|
||||
icon={icon}
|
||||
children={children}
|
||||
isActive={isActive}
|
||||
statusLabel={statusLabel}
|
||||
restProps={restProps}
|
||||
/>
|
||||
) : (
|
||||
<NonClickablePeriod
|
||||
periodRef={ref}
|
||||
start={start}
|
||||
end={endInclusive}
|
||||
status={status}
|
||||
cropped={cropped || ""}
|
||||
direction={direction}
|
||||
width={width}
|
||||
left={horizontalPosition}
|
||||
icon={icon}
|
||||
statusLabel={statusLabel}
|
||||
restProps={restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
) as PeriodType;
|
||||
|
||||
Period.componentType = "period";
|
||||
|
||||
export default Period;
|
||||
@@ -0,0 +1,326 @@
|
||||
import { SuccessFilled } from "@navikt/ds-icons";
|
||||
import { useState } from "@storybook/addons";
|
||||
import { Meta } from "@storybook/react";
|
||||
import * as React from "react";
|
||||
import Timeline from "./Timeline";
|
||||
|
||||
export default {
|
||||
title: "ds-react-internal/Timeline",
|
||||
component: Timeline,
|
||||
argTypes: {},
|
||||
} as Meta;
|
||||
|
||||
const DummyLabel = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "239px",
|
||||
margin: 0,
|
||||
textAlign: "left",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
<p>Periode: 15.07.2019 - 25.07.2019</p>
|
||||
<p>Utbetalt: 12 345,00 kr</p>
|
||||
<p style={{ color: "red" }}>Dager igjen: 0</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const row1 = [
|
||||
{
|
||||
id: "1",
|
||||
start: new Date("Jan 1 2022"),
|
||||
end: new Date("Jan 31 2022"),
|
||||
status: "success",
|
||||
icon: <SuccessFilled aria-hidden />,
|
||||
statusLabel: "Sykemeldt",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
start: new Date("Feb 1 2022"),
|
||||
end: new Date("Mar 15 2022"),
|
||||
status: "danger",
|
||||
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
|
||||
statusLabel: "Sykemeldt",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
start: new Date("May 2 2022"),
|
||||
end: new Date("May 25 2022"),
|
||||
status: "success",
|
||||
icon: <SuccessFilled aria-hidden />,
|
||||
statusLabel: "Sykemeldt",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
start: new Date("Mar 1 2022"),
|
||||
end: new Date("Mar 31 2022"),
|
||||
status: "success",
|
||||
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
|
||||
icon: <SuccessFilled aria-hidden />,
|
||||
label: "test",
|
||||
statusLabel: "Sykemeldt",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
start: new Date("Jul 1 2022"),
|
||||
end: new Date("Jul 31 2022"),
|
||||
status: "warning",
|
||||
icon: <SuccessFilled aria-hidden />,
|
||||
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
|
||||
statusLabel: "Sykemeldt",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
start: new Date("Aug 1 2022"),
|
||||
end: new Date("Aug 30 2022"),
|
||||
status: "warning",
|
||||
icon: <SuccessFilled aria-hidden />,
|
||||
statusLabel: "Sykemeldt",
|
||||
},
|
||||
];
|
||||
|
||||
const row2 = [
|
||||
{
|
||||
id: "7",
|
||||
start: new Date("May 13 2022"),
|
||||
end: new Date("May 25 2022"),
|
||||
status: "warning",
|
||||
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
|
||||
icon: <SuccessFilled aria-hidden />,
|
||||
children: <DummyLabel />,
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
start: new Date("Feb 1 2022"),
|
||||
end: new Date("May 2 2022"),
|
||||
status: "neutral",
|
||||
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
|
||||
icon: <SuccessFilled aria-hidden />,
|
||||
children: <DummyLabel />,
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<div style={{ width: "80vw" }}>
|
||||
<Timeline>
|
||||
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
|
||||
{row1.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
icon={p.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
|
||||
{row2.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
icon={p.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
</Timeline>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithPins = () => {
|
||||
return (
|
||||
<div style={{ width: "80vw" }}>
|
||||
<Timeline>
|
||||
<Timeline.Pin date={new Date("Apr 15 2022")}>Pin 1</Timeline.Pin>
|
||||
<Timeline.Pin date={new Date("Jun 12 2022")}>Pin 2</Timeline.Pin>
|
||||
<Timeline.Pin date={new Date("Jul 28 2022")}>Pin 3</Timeline.Pin>
|
||||
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
|
||||
{row1.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
icon={p.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
|
||||
{row2.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
icon={p.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
</Timeline>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithZoom = () => {
|
||||
return (
|
||||
<div style={{ width: "80vw" }}>
|
||||
<Timeline>
|
||||
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
|
||||
{row1.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
icon={p.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
|
||||
{row2.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
icon={p.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
<Timeline.Zoom>
|
||||
<Timeline.Zoom.Button label="3 mnd" interval="month" count={3} />
|
||||
<Timeline.Zoom.Button label="7 mnd" interval="month" count={7} />
|
||||
<Timeline.Zoom.Button label="9 mnd" interval="month" count={9} />
|
||||
</Timeline.Zoom>
|
||||
</Timeline>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActivePeriod = () => {
|
||||
const [activePeriod, setActivePeriod] = useState(row1[0]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "80vw" }}>
|
||||
<Timeline>
|
||||
<Timeline.Pin date={new Date("Apr 15 2022")}>Pin 1</Timeline.Pin>
|
||||
<Timeline.Pin date={new Date("Jun 12 2022")}>Pin 2</Timeline.Pin>
|
||||
<Timeline.Pin date={new Date("Jul 28 2022")}>Pin 3</Timeline.Pin>
|
||||
<Timeline.Row label="Rad 1" icon={<SuccessFilled aria-hidden />}>
|
||||
{row1.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
onSelectPeriod={() => {
|
||||
setActivePeriod(p);
|
||||
p?.onSelectPeriod?.();
|
||||
}}
|
||||
icon={p.icon}
|
||||
isActive={activePeriod.id === p.id}
|
||||
statusLabel={p.statusLabel}
|
||||
>
|
||||
{p.children}
|
||||
</Timeline.Period>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
<Timeline.Row label="Rad 2">
|
||||
{row2.map((p: any, i) => {
|
||||
return (
|
||||
<Timeline.Period
|
||||
key={p.id}
|
||||
start={p.start}
|
||||
end={p.end}
|
||||
status={p.status}
|
||||
onSelectPeriod={() => {
|
||||
setActivePeriod(p);
|
||||
p?.onSelectPeriod?.();
|
||||
}}
|
||||
icon={p.icon}
|
||||
isActive={activePeriod.id === p.id}
|
||||
statusLabel={p.statusLabel}
|
||||
>
|
||||
{p.children}
|
||||
</Timeline.Period>
|
||||
);
|
||||
})}
|
||||
</Timeline.Row>
|
||||
<Timeline.Row label="Rad 3">
|
||||
<Timeline.Period
|
||||
start={new Date("Jan 1 2022")}
|
||||
end={new Date("Aug 20 2022")}
|
||||
status="info"
|
||||
icon={<SuccessFilled aria-hidden />}
|
||||
/>
|
||||
</Timeline.Row>
|
||||
</Timeline>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithDayLabels = () => {
|
||||
return (
|
||||
<div style={{ width: "80vw" }}>
|
||||
<Timeline>
|
||||
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
|
||||
<Timeline.Period
|
||||
start={new Date("Feb 4 2022")}
|
||||
end={new Date("Feb 13 2022")}
|
||||
status="success"
|
||||
/>
|
||||
</Timeline.Row>
|
||||
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
|
||||
<Timeline.Period
|
||||
start={new Date("Feb 17 2022")}
|
||||
end={new Date("Feb 22 2022")}
|
||||
status="warning"
|
||||
/>
|
||||
</Timeline.Row>
|
||||
</Timeline>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithYearLabels = () => {
|
||||
return (
|
||||
<div style={{ width: "80vw" }}>
|
||||
<Timeline>
|
||||
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
|
||||
<Timeline.Period
|
||||
start={new Date("Feb 4 2022")}
|
||||
end={new Date("Feb 13 2028")}
|
||||
status="success"
|
||||
/>
|
||||
</Timeline.Row>
|
||||
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
|
||||
<Timeline.Period
|
||||
start={new Date("Feb 17 2024")}
|
||||
end={new Date("Feb 22 2031")}
|
||||
status="warning"
|
||||
/>
|
||||
</Timeline.Row>
|
||||
</Timeline>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { differenceInMilliseconds } from "date-fns";
|
||||
|
||||
export const position = (date: Date, start: Date, endInclusive: Date) => {
|
||||
const diff = differenceInMilliseconds(endInclusive, start);
|
||||
return (differenceInMilliseconds(date, start) / diff) * 100;
|
||||
};
|
||||
|
||||
export const horizontalPositionAndWidth = (
|
||||
start: Date,
|
||||
endInclusive: Date,
|
||||
timelineStart: Date,
|
||||
timelineEndInclusive: Date
|
||||
) => {
|
||||
const horizontalPosition = position(
|
||||
start,
|
||||
timelineStart,
|
||||
timelineEndInclusive
|
||||
);
|
||||
const width =
|
||||
position(endInclusive, timelineStart, timelineEndInclusive) -
|
||||
horizontalPosition;
|
||||
return {
|
||||
horizontalPosition: horizontalPosition,
|
||||
width: width,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { PeriodProps } from "../period";
|
||||
import { Spatial } from "./types.internal";
|
||||
|
||||
export interface Positioned {
|
||||
horizontalPosition: number;
|
||||
direction: "left" | "right";
|
||||
}
|
||||
|
||||
export const isVisible = ({ horizontalPosition }: Positioned): boolean =>
|
||||
horizontalPosition <= 98 && horizontalPosition >= 0;
|
||||
|
||||
export const getFirstDate = (periods: any) => {
|
||||
return periods.sort(
|
||||
(a: PeriodProps, b: PeriodProps) => a.start.getTime() - b.start.getTime()
|
||||
)[0].start;
|
||||
};
|
||||
|
||||
export const getLastDate = (periods: any) => {
|
||||
return periods.sort(
|
||||
(a: PeriodProps, b: PeriodProps) => a.end.getTime() - b.end.getTime()
|
||||
)[periods.length - 1].end;
|
||||
};
|
||||
|
||||
export const withinADay = (date1: Date, date2: Date): boolean =>
|
||||
differenceInDays(date1, date2) <= 1;
|
||||
|
||||
export const invisiblePeriods = ({
|
||||
horizontalPosition,
|
||||
width,
|
||||
}: Positioned & Spatial) =>
|
||||
horizontalPosition >= 0 && horizontalPosition <= 100 && width > 0;
|
||||
@@ -0,0 +1 @@
|
||||
export { getFirstDate, getLastDate, isVisible, Positioned } from "./filter";
|
||||
@@ -0,0 +1,48 @@
|
||||
import cl from "clsx";
|
||||
import { format } from "date-fns";
|
||||
|
||||
export const getConditionalClasses = (
|
||||
cropped: string,
|
||||
direction: string,
|
||||
status: string
|
||||
) => {
|
||||
return cl(`navdsi-timeline__period navdsi-timeline__period--${status}`, {
|
||||
"navdsi-timeline__period--connected-both": cropped === "both",
|
||||
"navdsi-timeline__period--connected-right":
|
||||
(cropped === "right" && direction === "left") ||
|
||||
(cropped === "left" && direction === "right"),
|
||||
"navdsi-timeline__period--connected-left":
|
||||
(cropped === "left" && direction === "left") ||
|
||||
(cropped === "right" && direction === "right"),
|
||||
});
|
||||
};
|
||||
|
||||
const translateStatus = (status: string): string => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return "Suksess";
|
||||
case "warning":
|
||||
return "Advarsel";
|
||||
case "danger":
|
||||
return "Fare";
|
||||
case "info":
|
||||
return "Info";
|
||||
case "neutral":
|
||||
return "Nøytral";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
export const ariaLabel = (
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
status: string,
|
||||
statusLabel?: string
|
||||
): string => {
|
||||
const start = format(startDate, "dd.MM.yyyy");
|
||||
const end = format(endDate, "dd.MM.yyyy");
|
||||
return `${
|
||||
statusLabel ? statusLabel : translateStatus(status)
|
||||
} fra ${start} til ${end}`;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { differenceInMilliseconds } from "date-fns";
|
||||
import { Period, PositionedPeriod } from "./types.internal";
|
||||
|
||||
export const lastDate = (a: Date, b: Date): number =>
|
||||
differenceInMilliseconds(a, b);
|
||||
|
||||
export const lastPeriod = (a: PositionedPeriod, b: PositionedPeriod): number =>
|
||||
a.horizontalPosition - b.horizontalPosition;
|
||||
|
||||
export const lastSinglePeriod = (a: Period, b: Period): number =>
|
||||
differenceInMilliseconds(b.endInclusive, a.endInclusive);
|
||||
@@ -0,0 +1,83 @@
|
||||
import { omit } from "@navikt/ds-react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Period } from "./types.external";
|
||||
|
||||
type ParsedChild = {
|
||||
label?: string;
|
||||
icon?: React.ReactNode;
|
||||
headingTag: string;
|
||||
periods: Omit<Period, "id" | "endInclusive">[];
|
||||
restProps: any;
|
||||
ref: any;
|
||||
};
|
||||
|
||||
export const parseRows = (rowChildren: ReactNode[]) => {
|
||||
let parsedChildren: ParsedChild[] = [];
|
||||
rowChildren?.forEach((r: React.ReactNode) => {
|
||||
let periods = [];
|
||||
if (React.isValidElement(r) && r?.props?.children) {
|
||||
if (Array.isArray(r.props.children)) {
|
||||
for (let i = 0; i < r.props.children.length; i++) {
|
||||
const p = r.props.children[i];
|
||||
|
||||
periods.push({
|
||||
start: p?.props?.start,
|
||||
end: p?.props?.end,
|
||||
status: p?.props?.status || "neutral",
|
||||
onSelectPeriod: p.props?.onSelectPeriod,
|
||||
label: r.props.label,
|
||||
icon: p.props.icon,
|
||||
children: p.props.children,
|
||||
isActive: p.props.isActive,
|
||||
statusLabel: p.props.statusLabel,
|
||||
restProps: omit(p.props, [
|
||||
"start",
|
||||
"end",
|
||||
"status",
|
||||
"onSelectPeriod",
|
||||
"label",
|
||||
"icon",
|
||||
"children",
|
||||
"isActive",
|
||||
"statusLabel",
|
||||
]),
|
||||
ref: p?.ref,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
periods.push({
|
||||
start: r.props.children.props.start,
|
||||
end: r.props.children.props.end,
|
||||
status: r.props.children.props?.status || "neutral",
|
||||
onSelectPeriod: r.props.children.props?.onSelectPeriod,
|
||||
label: r.props.label,
|
||||
icon: r.props.children.props?.icon,
|
||||
children: r.props.children.props?.children,
|
||||
statusLabel: r.props.children.props?.statusLabel,
|
||||
restProps: omit(r.props.children.props, [
|
||||
"start",
|
||||
"end",
|
||||
"status",
|
||||
"onSelectPeriod",
|
||||
"label",
|
||||
"icon",
|
||||
"children",
|
||||
"isActive",
|
||||
"statusLabel",
|
||||
]),
|
||||
ref: r.props?.children?.ref,
|
||||
});
|
||||
}
|
||||
parsedChildren.push({
|
||||
label: r.props.label,
|
||||
icon: r.props.icon,
|
||||
headingTag: r.props.headingTag,
|
||||
periods: periods,
|
||||
restProps: omit(r.props, ["label", "icon", "headingTag"]),
|
||||
ref: (r as any)?.ref,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return parsedChildren;
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
|
||||
export type PeriodStatus =
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "info"
|
||||
| "neutral";
|
||||
|
||||
export type Percentage = number;
|
||||
|
||||
export interface Positioned {
|
||||
horizontalPosition: number;
|
||||
direction: "left" | "right";
|
||||
}
|
||||
|
||||
export interface Period {
|
||||
id: string;
|
||||
label?: string;
|
||||
start: Date;
|
||||
endInclusive: Date;
|
||||
status?: PeriodStatus;
|
||||
onSelectPeriod?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
end: Date;
|
||||
isActive?: boolean;
|
||||
statusLabel?: string;
|
||||
}
|
||||
|
||||
export interface PositionedPeriod extends Period, Positioned {
|
||||
width: number;
|
||||
active?: boolean;
|
||||
cropped?: "left" | "right" | "both";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
hoverLabel?: React.ReactNode;
|
||||
infoPin?: boolean;
|
||||
start: Date;
|
||||
}
|
||||
|
||||
export interface Spatial {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface AxisLabel extends Positioned, Spatial {
|
||||
label: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface InternalSimpleTimeline {
|
||||
id: string;
|
||||
periods: PositionedPeriod[];
|
||||
}
|
||||
|
||||
export interface SimplePeriod {
|
||||
/**
|
||||
* Startdato for perioden, mao. periodens høyre kant.
|
||||
*/
|
||||
start: Date;
|
||||
/**
|
||||
* Sluttdato for perioden, mao. periodens venstre kant.
|
||||
*/
|
||||
end: Date;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
|
||||
export type TimelineComponentTypes = "row" | "pin" | "period";
|
||||
|
||||
export type PeriodStatus =
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "info"
|
||||
| "neutral";
|
||||
|
||||
export type Percentage = number;
|
||||
|
||||
export interface Positioned {
|
||||
horizontalPosition: number;
|
||||
direction: "left" | "right";
|
||||
}
|
||||
|
||||
export interface Period {
|
||||
id: string;
|
||||
start: Date;
|
||||
endInclusive: Date;
|
||||
status?: PeriodStatus;
|
||||
onSelectPeriod?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface PositionedPeriod extends Period, Positioned {
|
||||
width: number;
|
||||
active?: boolean;
|
||||
cropped?: "left" | "right" | "both";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
hoverLabel?: React.ReactNode;
|
||||
infoPin?: boolean;
|
||||
end: Date;
|
||||
statusLabel?: string;
|
||||
restProps?: any;
|
||||
ref?: any;
|
||||
}
|
||||
|
||||
export interface Spatial {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface AxisLabel extends Positioned, Spatial {
|
||||
label: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface InternalSimpleTimeline {
|
||||
label: string;
|
||||
id: string;
|
||||
periods: PositionedPeriod[];
|
||||
icon?: React.ReactNode;
|
||||
headingTag: "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
restProps: any;
|
||||
ref: any;
|
||||
}
|
||||
|
||||
export interface SelectedPeriod {
|
||||
/**
|
||||
* Start of the period.
|
||||
*/
|
||||
start: Date;
|
||||
/**
|
||||
* End of the period.
|
||||
*/
|
||||
end: Date;
|
||||
/**
|
||||
* Period id
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Detail } from "@navikt/ds-react";
|
||||
import {
|
||||
addMonths,
|
||||
addYears,
|
||||
format,
|
||||
isSameDay,
|
||||
subMonths,
|
||||
subYears,
|
||||
} from "date-fns";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTimelineContext } from "../hooks/useTimelineContext";
|
||||
|
||||
export interface TimelineZoomButtonProps {
|
||||
/**
|
||||
* e.g 3mnd, 6mnd etc
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Zoom interval in months or years
|
||||
*/
|
||||
interval: "month" | "year";
|
||||
/**
|
||||
* How many units of the interval that will be applied
|
||||
* e.g interval="month" + count={3} equals 3 months
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type ZoomButtonType = React.ForwardRefExoticComponent<
|
||||
TimelineZoomButtonProps & React.RefAttributes<HTMLButtonElement>
|
||||
>;
|
||||
|
||||
export const ZoomButton: ZoomButtonType = forwardRef(
|
||||
({ label, interval, count, ...rest }, ref) => {
|
||||
const { setStart, endDate, startDate, direction } = useTimelineContext();
|
||||
|
||||
let startOfRange: Date;
|
||||
|
||||
if (interval === "month") {
|
||||
startOfRange =
|
||||
direction === "left"
|
||||
? subMonths(endDate, count)
|
||||
: addMonths(startDate, count);
|
||||
} else {
|
||||
startOfRange =
|
||||
direction === "left"
|
||||
? subYears(endDate, count)
|
||||
: addYears(startDate, count);
|
||||
}
|
||||
|
||||
const currentZoom =
|
||||
direction === "left"
|
||||
? isSameDay(startDate, startOfRange)
|
||||
: isSameDay(endDate, startOfRange);
|
||||
|
||||
return (
|
||||
<Detail as="li">
|
||||
<button
|
||||
aria-label={
|
||||
!currentZoom
|
||||
? `Zoom tidslinjen ${format(
|
||||
startOfRange,
|
||||
"dd.MM.yyyy"
|
||||
)} til ${format(endDate, "dd.MM.yyyy")}`
|
||||
: "Tilbakestill til initiell tidsperspektiv"
|
||||
}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className="navdsi-timeline__zoom-button"
|
||||
onClick={() => {
|
||||
setStart(startOfRange);
|
||||
}}
|
||||
aria-pressed={currentZoom}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</Detail>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ZoomButton;
|
||||
@@ -0,0 +1,34 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import ZoomButton, { ZoomButtonType } from "./ZoomButton";
|
||||
|
||||
interface ZoomProps extends React.HTMLAttributes<HTMLUListElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ZoomType<Props = ZoomProps>
|
||||
extends React.ForwardRefExoticComponent<
|
||||
Props & React.RefAttributes<HTMLUListElement>
|
||||
> {
|
||||
Button: ZoomButtonType;
|
||||
componentType: string;
|
||||
}
|
||||
|
||||
export const Zoom = forwardRef<HTMLUListElement, ZoomProps>(
|
||||
({ className, children, ...rest }, ref) => {
|
||||
return (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cl(className, "navdsi-timeline__zoom")}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
) as ZoomType;
|
||||
|
||||
Zoom.Button = ZoomButton;
|
||||
Zoom.componentType = "zoom";
|
||||
|
||||
export default Zoom;
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
aksel.nav.no/
|
||||
├── website # NEXT.JS app (v12)
|
||||
└── sanity # Sanity CMS-app (v2)
|
||||
└── website/sanity # Sanity CMS-app (v3)
|
||||
```
|
||||
|
||||
## Commands
|
||||
@@ -71,7 +71,7 @@ Vi tar gjerne i mot PR-er [CONTRIBUTING.md](https://github.com/navikt/Designsyst
|
||||
|
||||
## Codeowners
|
||||
|
||||
Løsningen forvaltes av team Aksel/Designsystemet
|
||||
Løsningen forvaltes av team Aksel
|
||||
|
||||
[Slack](https://nav-it.slack.com/archives/C7NE7A8UF)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.4-alpine3.14
|
||||
FROM node:16.13.1-alpine3.14
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
ENV NODE_ENV production
|
||||
@@ -8,7 +8,6 @@ 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/
|
||||
|
||||
+7
-37
@@ -1,22 +1,17 @@
|
||||
CMS og nettside for [aksel.nav.no](https://aksel.nav.no/)
|
||||
|
||||
```sh
|
||||
├── /website # NEXT.JS app (v12)
|
||||
└── /sanity # Sanity CMS-app (v2)
|
||||
├── /website # NEXT.JS app (v12)
|
||||
└── /website/sanity # Sanity CMS-app (v3)
|
||||
```
|
||||
|
||||
### /website
|
||||
|
||||
[Nextjs](https://nextjs.org/) applikasjon som henter data fra et [sanity-datasett](https://www.sanity.io/).
|
||||
|
||||
Siden leverer to løsninger:
|
||||
### /website/sanity
|
||||
|
||||
- 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).
|
||||
Det publiserte studioet finner man på [aksel.nav.no/admin](https://aksel.nav.no/admin)(krever NAV SSO eller invite).
|
||||
|
||||
`production`-datasettet er privat, så man må ha riktige tilganger for å kunne lese/jobbe med dataen der.
|
||||
|
||||
@@ -24,7 +19,7 @@ Det publiserte studioet finner man på [aksel.nav.no/studio](https://aksel.nav.n
|
||||
|
||||
### 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).
|
||||
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). Kan også publisere andre branches til dev ved bruk av workflow-dispatch.
|
||||
|
||||
### Nettside (localhost:3000)
|
||||
|
||||
@@ -42,40 +37,15 @@ 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
|
||||
- SANITY_PREVIEW_TOKEN: Gir appen tilgang til å lese draft innhold fra sanity i "preview"-mode. Trengs for å teste preview-sider
|
||||
For å sende dokumenter til sanity, brukt til å oppdatere sandboxes/examples/farger. Trengs bare hvis kode skal synkes fra lokal branch/teste feedback-løsning
|
||||
- SANITY_PREVIEW_TOKEN: Gir appen tilgang til å lese draft innhold fra sanity i "preview"-mode. Trengs for å teste ikke publiserte-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`
|
||||
|
||||
@@ -12,24 +12,11 @@ spec:
|
||||
- "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:
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
"website"
|
||||
],
|
||||
"dependencies": {
|
||||
"@navikt/ds-codemod": "^2.0.18",
|
||||
"@navikt/ds-css": "^2.0.18",
|
||||
"@navikt/ds-css-internal": "^2.0.18",
|
||||
"@navikt/ds-icons": "^2.0.18",
|
||||
"@navikt/ds-react": "^2.0.18",
|
||||
"@navikt/ds-react-internal": "^2.0.18",
|
||||
"@navikt/ds-tailwind": "^2.0.18",
|
||||
"@navikt/ds-tokens": "^2.0.18"
|
||||
"@navikt/ds-codemod": "^2.1.6",
|
||||
"@navikt/ds-css": "^2.1.6",
|
||||
"@navikt/ds-css-internal": "^2.1.6",
|
||||
"@navikt/ds-icons": "^2.1.6",
|
||||
"@navikt/ds-react": "^2.1.6",
|
||||
"@navikt/ds-react-internal": "^2.1.6",
|
||||
"@navikt/ds-tailwind": "^2.1.6",
|
||||
"@navikt/ds-tokens": "^2.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier-plugin-tailwindcss": "^0.1.13"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
.yarn/cache
|
||||
*.tar.gz
|
||||
Binary file not shown.
@@ -1,46 +0,0 @@
|
||||
// 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]);
|
||||
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
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-designsystemet 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>
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
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];
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import config from "config:sanity";
|
||||
|
||||
const BUNDLE_CHECK_INTERVAL = 60 * 5000;
|
||||
const BUNDLE_CHECK_INTERVAL_DECLINED = 60 * 20000;
|
||||
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);
|
||||
|
||||
/*
|
||||
TODO: @release uncomment for redirecting to new studio
|
||||
*/
|
||||
/* window.location.replace(`http://aksel.nav.no/admin`); */
|
||||
}, []);
|
||||
|
||||
// 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
|
||||
);
|
||||
@@ -1,44 +0,0 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
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.listItem()
|
||||
.title("Standalone-sider")
|
||||
.child(S.documentTypeList("aksel_standalone")),
|
||||
S.listItem().title("Redirects").child(S.documentTypeList("redirect")),
|
||||
])
|
||||
);
|
||||
@@ -1,104 +0,0 @@
|
||||
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("Produktbloggen")
|
||||
.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")),
|
||||
])
|
||||
);
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
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]);
|
||||
});
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
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("Props")
|
||||
.icon(() => <Folder />)
|
||||
.child(S.documentTypeList("ds_props")),
|
||||
S.listItem()
|
||||
.title("Token-kategorier")
|
||||
.icon(() => <Folder />)
|
||||
.child(S.documentTypeList("token_kategori")),
|
||||
])
|
||||
),
|
||||
S.divider(),
|
||||
S.documentListItem()
|
||||
.title(`Komponent-template`)
|
||||
.schemaType(`ds_component_template`)
|
||||
.icon(() => <Task />)
|
||||
.id(`ds_component_templateid`),
|
||||
])
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
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",
|
||||
};
|
||||
},
|
||||
}),
|
||||
];
|
||||
@@ -1,38 +0,0 @@
|
||||
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() */
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
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];
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
@@ -1,30 +0,0 @@
|
||||
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);
|
||||
@@ -1,49 +0,0 @@
|
||||
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;
|
||||
@@ -1,18 +0,0 @@
|
||||
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"));
|
||||
@@ -1,147 +0,0 @@
|
||||
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/C7NE7A8UF">
|
||||
#Aksel-designsystemet
|
||||
</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}>
|
||||
<Heading level="3" size="xsmall" style={{ display: "flex" }}>
|
||||
<Upload />
|
||||
Publiserte sider
|
||||
</Heading>
|
||||
|
||||
<ReadMore header={`Oppdaterte: ${published.length}`}>
|
||||
{published ? (
|
||||
<ReferringDocumentsList documents={published} />
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</ReadMore>
|
||||
<ReadMore header={`Med upubliserte endringer: ${drafts.length}`}>
|
||||
{drafts ? (
|
||||
<ReferringDocumentsList documents={drafts} />
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</ReadMore>
|
||||
<Heading level="3" size="xsmall" className={styles.headings}>
|
||||
<Wrench />
|
||||
Under arbeid
|
||||
</Heading>
|
||||
<ReadMore header={`Sider: ${unpublished.length}`}>
|
||||
{unpublished ? (
|
||||
<ReferringDocumentsList documents={unpublished} />
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</ReadMore>
|
||||
<Heading level="3" size="xsmall" className={styles.headings}>
|
||||
<Sandglass />
|
||||
Utdaterte sider
|
||||
</Heading>
|
||||
<ReadMore header={`Ikke oppdatert på over 90 dager: 0`}>
|
||||
<EmptyState />
|
||||
</ReadMore>
|
||||
<ReadMore header={`Ikke oppdatert på over 180 dager: 0`}>
|
||||
<EmptyState />
|
||||
</ReadMore>
|
||||
</Stack>
|
||||
);
|
||||
}}
|
||||
</WithReferringDocuments>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
export default withDocument(IntroPage);
|
||||
@@ -1,35 +0,0 @@
|
||||
/* ./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(--a-surface-action-subtle-hover, 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(--a-shadow-focus, var(--a-shadow-focus, 0 0 0 3px rgba(0, 52, 125, 1)));
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
@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%;
|
||||
}
|
||||
|
||||
.headings {
|
||||
margin-top: 1rem !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
p.detailsContent {
|
||||
margin-left: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
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);
|
||||
@@ -1,41 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"#": "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",
|
||||
"table": "e56785cc6cecbacf8ebe6bb99ffae32b973e233503c0714497ac7df3042863f1"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"listOptions": {}
|
||||
}
|
||||
Loaded 100 of 283 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user