mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 01:02:02 +00:00
Compare commits
211
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 | ||
|
|
ffac16cb05 | ||
|
|
98177d0cd3 | ||
|
|
2c5be718e6 | ||
|
|
019d92dd7b | ||
|
|
5b72afbb93 | ||
|
|
56348cdddf | ||
|
|
973d79ee99 | ||
|
|
5bfb912531 | ||
|
|
27e3dfee07 | ||
|
|
20b2bf6f89 | ||
|
|
4b870986c8 | ||
|
|
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,23 @@
|
||||
# @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
|
||||
|
||||
## 2.0.16
|
||||
|
||||
## 2.0.15
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-codemod",
|
||||
"version": "2.0.16",
|
||||
"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.16",
|
||||
"@navikt/ds-react": "^2.0.16",
|
||||
"@navikt/ds-icons": "^2.1.6",
|
||||
"@navikt/ds-react": "^2.1.6",
|
||||
"clsx": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @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
|
||||
|
||||
## 2.0.16
|
||||
|
||||
## 2.0.15
|
||||
|
||||
@@ -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.16",
|
||||
"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.16",
|
||||
"@navikt/ds-tokens": "^2.1.6",
|
||||
"normalize.css": "^8.0.1",
|
||||
"postcss": "^8.4.0",
|
||||
"postcss-cli": "^9.0.0",
|
||||
|
||||
@@ -1,5 +1,84 @@
|
||||
# @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
|
||||
|
||||
- [`98177d0cd`](https://github.com/navikt/Designsystemet/commit/98177d0cd3802029dcf398ce93c1767d3b0860c5) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Bedre typer for ref i datepicker
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-icons@2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de)]:
|
||||
- @navikt/ds-icons@2.0.17
|
||||
|
||||
## 2.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "2.0.16",
|
||||
"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.16",
|
||||
"@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 {
|
||||
@@ -59,7 +71,10 @@ interface UseDatepickerValue {
|
||||
/**
|
||||
* Use: <DatePicker.Input {...inputProps}/>
|
||||
*/
|
||||
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "onBlur" | "value">;
|
||||
inputProps: Pick<
|
||||
DateInputProps,
|
||||
"onChange" | "onFocus" | "onBlur" | "value"
|
||||
> & { ref: React.RefObject<HTMLInputElement> };
|
||||
/**
|
||||
* Resets all states (callback)
|
||||
*/
|
||||
@@ -112,6 +127,8 @@ export const useDatepicker = (
|
||||
inputFormat,
|
||||
onValidate,
|
||||
defaultMonth,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const locale = getLocaleFromString(_locale);
|
||||
@@ -187,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));
|
||||
@@ -196,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));
|
||||
};
|
||||
@@ -227,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 {
|
||||
@@ -45,7 +57,9 @@ interface UseMonthPickerValue {
|
||||
/**
|
||||
* Use: <MonthPicker.Input {...inputProps} />
|
||||
*/
|
||||
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value">;
|
||||
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value"> & {
|
||||
ref: React.RefObject<HTMLInputElement>;
|
||||
};
|
||||
/**
|
||||
* Currently selected Date
|
||||
* Up to user to validate value and extract month
|
||||
@@ -94,6 +108,8 @@ export const useMonthpicker = (
|
||||
inputFormat,
|
||||
onValidate,
|
||||
defaultYear,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
|
||||
@@ -101,7 +117,7 @@ export const useMonthpicker = (
|
||||
const today = new Date();
|
||||
const locale = getLocaleFromString(_locale);
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const monthpickerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Initialize states
|
||||
@@ -170,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));
|
||||
};
|
||||
@@ -189,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);
|
||||
@@ -209,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,6 +1,7 @@
|
||||
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
||||
import isBefore from "date-fns/isBefore";
|
||||
import isWeekend from "date-fns/isWeekend";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { DateRange, isMatch } from "react-day-picker";
|
||||
import { DateInputProps } from "../DateInput";
|
||||
import { DatePickerProps } from "../datepicker/DatePicker";
|
||||
@@ -71,14 +72,14 @@ interface UseRangeDatepickerValue {
|
||||
fromInputProps: Pick<
|
||||
DateInputProps,
|
||||
"onChange" | "onFocus" | "onBlur" | "value"
|
||||
>;
|
||||
> & { ref: React.RefObject<HTMLInputElement> };
|
||||
/**
|
||||
* Use: <DatePicker.Input label="to" {...toInputProps}/>
|
||||
*/
|
||||
toInputProps: Pick<
|
||||
DateInputProps,
|
||||
"onChange" | "onFocus" | "onBlur" | "value"
|
||||
>;
|
||||
> & { ref: React.RefObject<HTMLInputElement> };
|
||||
/**
|
||||
* Resets all states (callback)
|
||||
*/
|
||||
@@ -202,12 +203,14 @@ export const useRangeDatepicker = (
|
||||
inputFormat,
|
||||
onValidate,
|
||||
defaultMonth,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const locale = getLocaleFromString(_locale);
|
||||
|
||||
const inputRefTo = useRef<HTMLDivElement>(null);
|
||||
const inputRefFrom = useRef<HTMLDivElement>(null);
|
||||
const inputRefTo = useRef<HTMLInputElement>(null);
|
||||
const inputRefFrom = useRef<HTMLInputElement>(null);
|
||||
const datePickerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
|
||||
@@ -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,23 @@
|
||||
# @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
|
||||
|
||||
## 2.0.16
|
||||
|
||||
## 2.0.15
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "2.0.16",
|
||||
"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.16",
|
||||
"@navikt/ds-tokens": "^2.1.6",
|
||||
"@types/jest": "^29.0.0",
|
||||
"color": "4.2.3",
|
||||
"jest": "^29.0.0",
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @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
|
||||
|
||||
## 2.0.16
|
||||
|
||||
## 2.0.15
|
||||
|
||||
@@ -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.16",
|
||||
"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,31 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Translate og Paragraph ikon
|
||||
|
||||
## 2.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
+26
-14
@@ -867,13 +867,13 @@
|
||||
"name": "Eu",
|
||||
"description": "",
|
||||
"created_at": "2022-02-08T11:45:47.104Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "EuFilled",
|
||||
"description": "",
|
||||
"created_at": "2022-02-08T11:45:47.109Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "Expand",
|
||||
@@ -1479,13 +1479,13 @@
|
||||
"name": "Law",
|
||||
"description": "",
|
||||
"created_at": "2020-07-01T10:37:42.819Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "LawFilled",
|
||||
"description": "",
|
||||
"created_at": "2021-01-11T13:37:55.330Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "Left",
|
||||
@@ -1551,37 +1551,37 @@
|
||||
"name": "Locked",
|
||||
"description": "",
|
||||
"created_at": "2020-07-01T10:37:43.027Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "LockedFilled",
|
||||
"description": "",
|
||||
"created_at": "2021-01-11T13:37:55.325Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "Login",
|
||||
"description": "",
|
||||
"created_at": "2020-07-01T10:37:43.087Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "LoginFilled",
|
||||
"description": "",
|
||||
"created_at": "2021-01-11T13:37:55.531Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "Logout",
|
||||
"description": "",
|
||||
"created_at": "2020-07-01T10:37:42.931Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "LogoutFilled",
|
||||
"description": "",
|
||||
"created_at": "2021-01-11T13:37:55.609Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "Male",
|
||||
@@ -1811,6 +1811,12 @@
|
||||
"created_at": "2021-12-15T15:16:55.869Z",
|
||||
"pageName": "People"
|
||||
},
|
||||
{
|
||||
"name": "Paragraph",
|
||||
"description": "",
|
||||
"created_at": "2023-01-17T13:24:22.989Z",
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "Parking",
|
||||
"description": "",
|
||||
@@ -1827,13 +1833,13 @@
|
||||
"name": "Passport",
|
||||
"description": "",
|
||||
"created_at": "2020-07-01T10:37:43.078Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "PassportFilled",
|
||||
"description": "",
|
||||
"created_at": "2021-01-11T13:37:55.757Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "PensionBag",
|
||||
@@ -2615,6 +2621,12 @@
|
||||
"created_at": "2021-01-28T14:00:38.585Z",
|
||||
"pageName": "Buildings"
|
||||
},
|
||||
{
|
||||
"name": "Translate",
|
||||
"description": "",
|
||||
"created_at": "2023-01-18T10:52:22.311Z",
|
||||
"pageName": "Interface"
|
||||
},
|
||||
{
|
||||
"name": "Truck",
|
||||
"description": "",
|
||||
@@ -2643,13 +2655,13 @@
|
||||
"name": "Unlocked",
|
||||
"description": "",
|
||||
"created_at": "2020-07-01T10:37:42.823Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "UnlockedFilled",
|
||||
"description": "",
|
||||
"created_at": "2021-01-11T13:37:55.317Z",
|
||||
"pageName": "Privacy and secutity"
|
||||
"pageName": "Privacy and security"
|
||||
},
|
||||
{
|
||||
"name": "Up",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-icons",
|
||||
"version": "2.0.16",
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.79085 2 7.99999 3.79086 7.99999 6C7.99999 6.66187 8.16311 7.29714 8.45828 7.85918C6.00646 9.12956 5.6173 13.1739 8.76271 14.4845L12.7692 16.1538C13.5145 16.4644 14 17.1926 14 18C14 19.1046 13.1046 20 12 20C10.8954 20 9.99999 19.1046 9.99999 18H7.99999C7.99999 20.2091 9.79085 22 12 22C14.2091 22 16 20.2091 16 18C16 17.3381 15.8369 16.7029 15.5417 16.1408C17.9935 14.8704 18.3827 10.8261 15.2373 9.51554L11.2308 7.84615C10.4855 7.53561 9.99999 6.8074 9.99999 6C9.99999 4.89543 10.8954 4 12 4C13.1046 4 14 4.89543 14 6H16C16 3.79086 14.2091 2 12 2ZM13.9738 14.5208C15.6075 14.4232 16.1245 12.0519 14.4681 11.3617L10.4615 9.69231C10.311 9.62957 10.1657 9.55832 10.0262 9.47924C8.39252 9.57681 7.87544 11.9481 9.53194 12.6383L13.5385 14.3077C13.689 14.3704 13.8343 14.4417 13.9738 14.5208Z" fill="#262626"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 965 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 19.5L15.5 10.5L19.5 19.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 17H18" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<mask id="mask0_6814_1365" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="4" width="15" height="17">
|
||||
<path d="M9.90083 18.7893L13.9008 9.78926C14.1817 9.15728 14.8084 8.75 15.5 8.75C16.1193 8.75 16.6865 9.07657 17.0001 9.59861V4H2.00005V21H10.5983C9.85068 20.5519 9.53784 19.606 9.90083 18.7893Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_6814_1365)">
|
||||
<path d="M4 7.5H16" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 7.5V5.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5 7.5L13.4116 7.78744C12.2005 11.7232 9.30115 14.9162 5.5 16.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 7.5L6.58844 7.78744C7.79946 11.7232 10.6989 14.9162 14.5 16.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +1,28 @@
|
||||
# @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
|
||||
|
||||
## 2.0.16
|
||||
|
||||
## 2.0.15
|
||||
|
||||
@@ -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.16",
|
||||
"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,82 @@
|
||||
# @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
|
||||
|
||||
- Updated dependencies [[`98177d0cd`](https://github.com/navikt/Designsystemet/commit/98177d0cd3802029dcf398ce93c1767d3b0860c5)]:
|
||||
- @navikt/ds-react@2.0.18
|
||||
- @navikt/ds-icons@2.0.18
|
||||
|
||||
## 2.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de)]:
|
||||
- @navikt/ds-icons@2.0.17
|
||||
- @navikt/ds-react@2.0.17
|
||||
|
||||
## 2.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react-internal",
|
||||
"version": "2.0.16",
|
||||
"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.16",
|
||||
"@navikt/ds-react": "^2.0.16",
|
||||
"@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.16",
|
||||
"@navikt/ds-css": "^2.0.16",
|
||||
"@navikt/ds-css-internal": "^2.0.16",
|
||||
"@navikt/ds-icons": "^2.0.16",
|
||||
"@navikt/ds-react": "^2.0.16",
|
||||
"@navikt/ds-react-internal": "^2.0.16",
|
||||
"@navikt/ds-tailwind": "^2.0.16",
|
||||
"@navikt/ds-tokens": "^2.0.16"
|
||||
"@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);
|
||||
Loaded 100 of 288 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user