Skip to content

Commit 6838c26

Browse files
Merge pull request #2029 from zetkin/main
Release
2 parents 0ae9a9b + f2247a8 commit 6838c26

File tree

212 files changed

+8056
-1542
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

212 files changed

+8056
-1542
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Dockerfile
22
node_modules
33
.git
4+
.github
45
.next
56
env

.env.development

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
# Client settings
2-
NEXT_PUBLIC_APP_USE_TLS=0
1+
ZETKIN_APP_DOMAIN=http://www.dev.zetkin.org
32

43
# Zetkin API settings
54
ZETKIN_API_DOMAIN=dev.zetkin.org
65
ZETKIN_API_HOST=api.dev.zetkin.org
7-
NEXT_PUBLIC_ZETKIN_APP_DOMAIN=http://www.dev.zetkin.org
86
ZETKIN_CLIENT_ID=a0db63a12bae45ff83d12de70c8992c0
97
ZETKIN_CLIENT_SECRET=MWQyZmE2M2UtMzM3Yi00ODUyLWI2NGMtOWY5YTY5NTY3YjU5
108
ZETKIN_APP_HOST=localhost:3000

.github/workflows/delivery.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Build docker image
2+
3+
on:
4+
push:
5+
branches:
6+
- "release"
7+
8+
env:
9+
REGISTRY: ghcr.io
10+
IMAGE_NAME: ${{ github.repository }}
11+
12+
jobs:
13+
build-and-push-image:
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
packages: write
18+
attestations: write
19+
id-token: write
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
24+
# Buildx is necessary for GitHub Actions caching to work
25+
- name: Set up Docker Buildx
26+
uses: docker/setup-buildx-action@v3
27+
28+
- name: Log in to the Container registry
29+
uses: docker/login-action@v3
30+
with:
31+
registry: ${{ env.REGISTRY }}
32+
username: ${{ github.actor }}
33+
password: ${{ secrets.GITHUB_TOKEN }}
34+
35+
- name: Extract metadata (tags, labels) for Docker
36+
id: meta
37+
uses: docker/metadata-action@v5
38+
with:
39+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
40+
# The image will be tagged YYMMDD and YYMMDD-abcdef,
41+
# where abcdef is the shortform hash of the last commit
42+
tags: |
43+
type=sha,prefix={{date 'YYMMDD'}}-
44+
type=raw,value={{date 'YYMMDD'}}
45+
type=raw,value=latest
46+
47+
- name: Build and push Docker image
48+
id: push
49+
uses: docker/build-push-action@v5
50+
with:
51+
context: .
52+
file: ./env/frontend/Dockerfile
53+
push: true
54+
tags: ${{ steps.meta.outputs.tags }}
55+
labels: ${{ steps.meta.outputs.labels }}
56+
cache-from: type=gha
57+
cache-to: type=gha,mode=max
58+
59+
- name: Generate artifact attestation
60+
uses: actions/attest-build-provenance@v1
61+
with:
62+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
63+
subject-digest: ${{ steps.push.outputs.digest }}
64+
push-to-registry: true

env/frontend/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ COPY yarn.lock /var/app/yarn.lock
77
RUN yarn install --no-cache --frozen-lockfile
88

99
COPY . /var/app
10+
RUN yarn build
1011

1112
CMD ./run.sh

run.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
if [[ "$NODE_ENV" == "production" ]];
44
then
5-
yarn build
65
yarn start
76
else
87
yarn dev

src/app/layout.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@ export default async function RootLayout({
2929
<html lang="en">
3030
<body>
3131
<AppRouterCacheProvider>
32-
<ClientContext lang={lang} messages={messages} user={user}>
32+
<ClientContext
33+
envVars={{
34+
MUIX_LICENSE_KEY: process.env.MUIX_LICENSE_KEY || null,
35+
ZETKIN_APP_DOMAIN: process.env.ZETKIN_APP_DOMAIN || null,
36+
}}
37+
lang={lang}
38+
messages={messages}
39+
user={user}
40+
>
3341
{children}
3442
</ClientContext>
3543
</AppRouterCacheProvider>

src/core/caching/cacheUtils.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,14 @@ export function loadListIfNecessary<
2020
actionOnError?: (err: unknown) => PayloadAction<unknown>;
2121
actionOnLoad: () => PayloadAction<OnLoadPayload>;
2222
actionOnSuccess: (items: DataType[]) => PayloadAction<OnSuccessPayload>;
23+
isNecessary?: () => boolean;
2324
loader: () => Promise<DataType[]>;
2425
}
2526
): IFuture<DataType[]> {
26-
if (!remoteList || shouldLoad(remoteList)) {
27-
dispatch(hooks.actionOnLoad());
28-
const promise = hooks
29-
.loader()
30-
.then((val) => {
31-
dispatch(hooks.actionOnSuccess(val));
32-
return val;
33-
})
34-
.catch((err: unknown) => {
35-
if (hooks.actionOnError) {
36-
dispatch(hooks.actionOnError(err));
37-
return null;
38-
} else {
39-
throw err;
40-
}
41-
});
27+
const loadIsNecessary = hooks.isNecessary?.() ?? shouldLoad(remoteList);
4228

43-
return new PromiseFuture(promise);
29+
if (!remoteList || loadIsNecessary) {
30+
return loadList(dispatch, hooks);
4431
}
4532

4633
return new RemoteListFuture({
@@ -49,6 +36,38 @@ export function loadListIfNecessary<
4936
});
5037
}
5138

39+
export function loadList<
40+
DataType,
41+
OnLoadPayload = void,
42+
OnSuccessPayload = DataType[]
43+
>(
44+
dispatch: AppDispatch,
45+
hooks: {
46+
actionOnError?: (err: unknown) => PayloadAction<unknown>;
47+
actionOnLoad: () => PayloadAction<OnLoadPayload>;
48+
actionOnSuccess: (items: DataType[]) => PayloadAction<OnSuccessPayload>;
49+
loader: () => Promise<DataType[]>;
50+
}
51+
): IFuture<DataType[]> {
52+
dispatch(hooks.actionOnLoad());
53+
const promise = hooks
54+
.loader()
55+
.then((val) => {
56+
dispatch(hooks.actionOnSuccess(val));
57+
return val;
58+
})
59+
.catch((err: unknown) => {
60+
if (hooks.actionOnError) {
61+
dispatch(hooks.actionOnError(err));
62+
return null;
63+
} else {
64+
throw err;
65+
}
66+
});
67+
68+
return new PromiseFuture(promise);
69+
}
70+
5271
export function loadItemIfNecessary<
5372
DataType,
5473
OnLoadPayload = void,

src/core/caching/shouldLoad.spec.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import shouldLoad from './shouldLoad';
2+
import { remoteItem, remoteList } from 'utils/storeUtils';
3+
4+
describe('shouldLoad()', () => {
5+
const dummyItemData = {
6+
id: 1,
7+
title: 'Dummy',
8+
};
9+
10+
describe('with lists', () => {
11+
it('returns true when list is undefined', () => {
12+
const result = shouldLoad(undefined);
13+
expect(result).toBeTruthy();
14+
});
15+
16+
it('returns true when list has not loaded', () => {
17+
const list = remoteList([dummyItemData]);
18+
list.loaded = null;
19+
20+
const result = shouldLoad(list);
21+
expect(result).toBeTruthy();
22+
});
23+
24+
it('returns true when list has loaded but is stale', () => {
25+
const list = remoteList([dummyItemData]);
26+
list.loaded = new Date().toISOString();
27+
list.isStale = true;
28+
29+
const result = shouldLoad(list);
30+
expect(result).toBeTruthy();
31+
});
32+
33+
it('returns true when list has loaded but too long ago', () => {
34+
const loaded = new Date();
35+
loaded.setMinutes(loaded.getMinutes() - 6);
36+
37+
const list = remoteList([dummyItemData]);
38+
list.loaded = loaded.toISOString();
39+
40+
const result = shouldLoad(list);
41+
expect(result).toBeTruthy();
42+
});
43+
44+
it('returns false when list has loaded recently', () => {
45+
const list = remoteList([dummyItemData]);
46+
list.loaded = new Date().toISOString();
47+
48+
const result = shouldLoad(list);
49+
expect(result).toBeFalsy();
50+
});
51+
52+
it('returns false when list is already loading', () => {
53+
const list = remoteList([dummyItemData]);
54+
list.isLoading = true;
55+
56+
const result = shouldLoad(list);
57+
expect(result).toBeFalsy();
58+
});
59+
});
60+
61+
describe('with items', () => {
62+
it('returns true when item is undefined', () => {
63+
const result = shouldLoad(undefined);
64+
expect(result).toBeTruthy();
65+
});
66+
67+
it('returns true when item has not loaded', () => {
68+
const item = remoteItem(dummyItemData.id);
69+
item.loaded = null;
70+
71+
const result = shouldLoad(item);
72+
expect(result).toBeTruthy();
73+
});
74+
75+
it('returns true when item has loaded but is stale', () => {
76+
const item = remoteItem(dummyItemData.id);
77+
item.loaded = new Date().toISOString();
78+
item.isStale = true;
79+
80+
const result = shouldLoad(item);
81+
expect(result).toBeTruthy();
82+
});
83+
84+
it('returns true when item has loaded but too long ago', () => {
85+
const loaded = new Date();
86+
loaded.setMinutes(loaded.getMinutes() - 6);
87+
88+
const item = remoteItem(dummyItemData.id);
89+
item.loaded = loaded.toISOString();
90+
item.isStale = true;
91+
92+
const result = shouldLoad(item);
93+
expect(result).toBeTruthy();
94+
});
95+
96+
it('returns false when item has loaded recently', () => {
97+
const item = remoteItem(dummyItemData.id);
98+
item.loaded = new Date().toISOString();
99+
100+
const result = shouldLoad(item);
101+
expect(result).toBeFalsy();
102+
});
103+
104+
it('returns false when item is already loading', () => {
105+
const item = remoteItem(dummyItemData.id);
106+
item.isLoading = true;
107+
108+
const result = shouldLoad(item);
109+
expect(result).toBeFalsy();
110+
});
111+
112+
it('returns false when item is deleted', () => {
113+
const item = remoteItem(dummyItemData.id);
114+
item.deleted = true;
115+
116+
const result = shouldLoad(item);
117+
expect(result).toBeFalsy();
118+
});
119+
});
120+
121+
describe('with map of lists', () => {
122+
it('returns true when any needs loading', () => {
123+
const map = {
124+
'1': remoteList([dummyItemData]),
125+
'2': remoteList([dummyItemData]),
126+
};
127+
128+
// List for 1 needs loading
129+
map[1].isStale = true;
130+
131+
const result = shouldLoad(map, [1, 2]);
132+
expect(result).toBeTruthy();
133+
});
134+
135+
it('returns false when none need loading', () => {
136+
const map = {
137+
'1': remoteList([dummyItemData]),
138+
'2': remoteList([dummyItemData]),
139+
};
140+
141+
// Both lists have loaded
142+
map[1].loaded = new Date().toISOString();
143+
map[2].loaded = new Date().toISOString();
144+
145+
const result = shouldLoad(map, [1, 2]);
146+
expect(result).toBeFalsy();
147+
});
148+
149+
it('returns true when ID is not in map', () => {
150+
const result = shouldLoad({}, [1, 2]);
151+
expect(result).toBeTruthy();
152+
});
153+
});
154+
155+
describe('with map of items', () => {
156+
it('returns true when any needs loading', () => {
157+
const map = {
158+
'1': remoteItem(dummyItemData.id),
159+
'2': remoteItem(dummyItemData.id),
160+
};
161+
162+
// List for 1 needs loading
163+
map[1].isStale = true;
164+
165+
const result = shouldLoad(map, [1, 2]);
166+
expect(result).toBeTruthy();
167+
});
168+
169+
it('returns false when none need loading', () => {
170+
const map = {
171+
'1': remoteItem(dummyItemData.id),
172+
'2': remoteItem(dummyItemData.id),
173+
};
174+
175+
// Both lists have loaded
176+
map[1].loaded = new Date().toISOString();
177+
map[2].loaded = new Date().toISOString();
178+
179+
const result = shouldLoad(map, [1, 2]);
180+
expect(result).toBeFalsy();
181+
});
182+
});
183+
});

0 commit comments

Comments
 (0)