@inwink-mcp/backoffice-extension-helpers 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -43,7 +43,7 @@ import fs from "fs";
43
43
  import path from "path";
44
44
  var MANIFEST_FILENAME = "extension.manifest.json";
45
45
  var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
46
- var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
46
+ var SEMVER_RE = /^(?:\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?|\d{4}\.\d{1,2}\.\d{1,2}\.\d+)$/;
47
47
  function isValidSlug(value) {
48
48
  return SLUG_RE.test(value);
49
49
  }
@@ -80,6 +80,33 @@ function getNameAndVersion(manifest) {
80
80
  }
81
81
  return { name, version };
82
82
  }
83
+ function updateVersionToDateFormat(projectDir) {
84
+ const manifest = readManifest(projectDir);
85
+ const oldVersion = typeof manifest.version === "string" ? manifest.version : "";
86
+ const now = /* @__PURE__ */ new Date();
87
+ const yyyy = now.getFullYear();
88
+ const m = now.getMonth() + 1;
89
+ const d = now.getDate();
90
+ const todayPrefix = `${yyyy}.${m}.${d}`;
91
+ let newVersion;
92
+ const versionMatch = oldVersion.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})\.(\d+)$/);
93
+ if (versionMatch) {
94
+ const [, oldYyyy, oldM, oldD, oldX] = versionMatch;
95
+ const oldDatePrefix = `${oldYyyy}.${oldM}.${oldD}`;
96
+ if (oldDatePrefix === todayPrefix) {
97
+ const newX = String(parseInt(oldX, 10) + 1);
98
+ newVersion = `${todayPrefix}.${newX}`;
99
+ } else {
100
+ newVersion = `${todayPrefix}.1`;
101
+ }
102
+ } else {
103
+ newVersion = `${todayPrefix}.1`;
104
+ }
105
+ const updatedManifest = { ...manifest, version: newVersion };
106
+ const manifestFile = manifestPath(projectDir);
107
+ fs.writeFileSync(manifestFile, JSON.stringify(updatedManifest, null, 2) + "\n", "utf8");
108
+ return newVersion;
109
+ }
83
110
 
84
111
  // src/lib/templates.ts
85
112
  import fs2 from "fs";
@@ -263,7 +290,10 @@ function bootstrap(opts) {
263
290
  fs3.mkdirSync(destDir, { recursive: true });
264
291
  }
265
292
  const now = /* @__PURE__ */ new Date();
266
- const defaultVersion = `${now.getFullYear()}.${String(now.getMonth() + 1)}.${String(now.getDate())}.1`;
293
+ const yyyy = now.getFullYear();
294
+ const m = now.getMonth() + 1;
295
+ const d = now.getDate();
296
+ const defaultVersion = `${yyyy}.${m}.${d}.1`;
267
297
  const placeholders = {
268
298
  name: opts.name,
269
299
  displayName: opts.displayName,
@@ -273,7 +303,7 @@ function bootstrap(opts) {
273
303
  communityApiUrl: opts.communityApiUrl,
274
304
  customerApiUrl: opts.customerApiUrl,
275
305
  version: defaultVersion
276
- // version par défaut au format YYYY.MM.DD
306
+ // version par défaut au format YYYY.M.D.x
277
307
  };
278
308
  let written;
279
309
  try {
@@ -478,6 +508,14 @@ function pack(opts) {
478
508
  error("Le build a \xE9chou\xE9 \u2014 corrigez les erreurs ci-dessus.");
479
509
  return buildCode;
480
510
  }
511
+ info("Mise \xE0 jour de la version avec la date du jour\u2026");
512
+ try {
513
+ version = updateVersionToDateFormat(projectDir);
514
+ success(`Nouvelle version : ${version}`);
515
+ } catch (err) {
516
+ error(err.message);
517
+ return 1;
518
+ }
481
519
  let zipPath;
482
520
  try {
483
521
  zipPath = zipDist(projectDir, name, version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inwink-mcp/backoffice-extension-helpers",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "author": "inwink",
5
5
  "license": "ISC",
6
6
  "description": "CLI pour bootstrapper, packager et vérifier les extensions backoffice inwink.",
@@ -16,7 +16,7 @@ if (!fs.existsSync(manifestPath)) {
16
16
  throw new Error('extension.manifest.json not found.');
17
17
  }
18
18
 
19
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
19
+ let manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
20
20
 
21
21
  if (typeof manifest.name !== 'string' || !manifest.name.trim()) {
22
22
  throw new Error('extension.manifest.json must define a non-empty name.');
@@ -26,7 +26,35 @@ if (typeof manifest.version !== 'string' || !manifest.version.trim()) {
26
26
  throw new Error('extension.manifest.json must define a non-empty version.');
27
27
  }
28
28
 
29
- const zipFileName = `${manifest.name}-${manifest.version}.zip`;
29
+ // Update version to YYYY.M.D.x format based on today's date
30
+ const now = new Date();
31
+ const yyyy = now.getFullYear();
32
+ const m = now.getMonth() + 1;
33
+ const d = now.getDate();
34
+ const todayPrefix = `${yyyy}.${m}.${d}`;
35
+
36
+ const oldVersion = manifest.version;
37
+ let newVersion;
38
+
39
+ const versionMatch = oldVersion.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})\.(\d+)$/);
40
+ if (versionMatch) {
41
+ const [, oldYyyy, oldM, oldD, oldX] = versionMatch;
42
+ const oldDatePrefix = `${oldYyyy}.${oldM}.${oldD}`;
43
+
44
+ if (oldDatePrefix === todayPrefix) {
45
+ // Same day: increment x
46
+ const newX = String(parseInt(oldX, 10) + 1);
47
+ newVersion = `${todayPrefix}.${newX}`;
48
+ } else {
49
+ // Different day: reset to 1
50
+ newVersion = `${todayPrefix}.1`;
51
+ }
52
+ } else {
53
+ // Old version is not in the correct format: start at 1
54
+ newVersion = `${todayPrefix}.1`;
55
+ }
56
+
57
+ const zipFileName = `${manifest.name}-${oldVersion}.zip`;
30
58
  const zipFilePath = path.join(projectRoot, zipFileName);
31
59
  const zip = new AdmZip();
32
60
 
@@ -56,4 +84,9 @@ zip.addLocalFolder(distDir);
56
84
  zip.addFile('source.zip', createSourceZipBuffer(projectRoot));
57
85
  zip.writeZip(zipFilePath);
58
86
 
59
- process.stdout.write(`${zipFileName}\n`);
87
+ // Update manifest with new version
88
+ manifest.version = newVersion;
89
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
90
+
91
+ process.stdout.write(`Version : ${oldVersion}\n`);
92
+ process.stdout.write(`Package : ${zipFileName}\n`);
@@ -1,5 +1,6 @@
1
1
  import { Navigate, Route, Routes } from 'react-router-dom';
2
2
  import ProtectedRoute from './auth/ProtectedRoute';
3
+ import Layout from './components/Layout';
3
4
  import HomePage from './pages/HomePage';
4
5
  // Pour appeler une API inwink, utilisez un hook dédié (jamais fetch directement) :
5
6
  // import { useInwinkEventApi } from './hooks/useInwinkEventApi';
@@ -9,14 +10,16 @@ import HomePage from './pages/HomePage';
9
10
  export default function App() {
10
11
  return (
11
12
  <Routes>
12
- <Route
13
- path="/"
14
- element={
15
- <ProtectedRoute>
16
- <HomePage />
17
- </ProtectedRoute>
18
- }
19
- />
13
+ <Route element={<Layout title="__DISPLAY_NAME__" />}>
14
+ <Route
15
+ path="/"
16
+ element={
17
+ <ProtectedRoute>
18
+ <HomePage />
19
+ </ProtectedRoute>
20
+ }
21
+ />
22
+ </Route>
20
23
  <Route path="*" element={<Navigate to="/" replace />} />
21
24
  </Routes>
22
25
  );
@@ -1,3 +1,4 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
1
2
  import { useAuth } from './useAuth';
2
3
 
3
4
  /** Erreur levée lorsqu'une réponse API n'est pas OK (hors 401 géré par renouvellement). */
@@ -36,66 +37,76 @@ async function safeBody(response: Response): Promise<unknown> {
36
37
  * sert aux hôtes externes déclarés ou à des bases d'URL spécifiques.
37
38
  */
38
39
  export function useInwinkApi(baseUrl: string): InwinkApi {
39
- const { getAccessToken, renewToken, login } = useAuth();
40
+ // Les fonctions d'auth sont lues via une ref (motif « latest ref », cf. `userRef`
41
+ // dans AuthProvider) : l'objet API renvoyé reste ainsi stable (mémoïsé sur `baseUrl`,
42
+ // une constante) tout en utilisant toujours les callbacks frais au moment de l'appel.
43
+ const auth = useAuth();
44
+ const authRef = useRef(auth);
45
+ useEffect(() => {
46
+ authRef.current = auth;
47
+ }, [auth]);
40
48
 
41
- async function doFetch(path: string, token: string, init?: RequestInit): Promise<Response> {
42
- return fetch(`${baseUrl}${path}`, {
43
- ...init,
44
- headers: {
45
- Authorization: `Bearer ${token}`,
46
- 'Content-Type': 'application/json',
47
- ...(init?.headers as Record<string, string> | undefined),
48
- },
49
- });
50
- }
51
-
52
- async function request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
53
- let token = getAccessToken();
54
- if (!token) {
55
- await login();
56
- throw new InwinkApiError(401, 'Non authentifié.');
49
+ return useMemo<InwinkApi>(() => {
50
+ async function doFetch(path: string, token: string, init?: RequestInit): Promise<Response> {
51
+ return fetch(`${baseUrl}${path}`, {
52
+ ...init,
53
+ headers: {
54
+ Authorization: `Bearer ${token}`,
55
+ 'Content-Type': 'application/json',
56
+ ...(init?.headers as Record<string, string> | undefined),
57
+ },
58
+ });
57
59
  }
58
60
 
59
- let response = await doFetch(path, token, init);
60
-
61
- if (response.status === 401) {
62
- const renewed = await renewToken();
63
- if (renewed) {
64
- token = renewed;
65
- response = await doFetch(path, token, init);
61
+ async function request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
62
+ const { getAccessToken, renewToken, login } = authRef.current;
63
+ let token = getAccessToken();
64
+ if (!token) {
65
+ await login();
66
+ throw new InwinkApiError(401, 'Non authentifié.');
66
67
  }
68
+
69
+ let response = await doFetch(path, token, init);
70
+
67
71
  if (response.status === 401) {
68
- await login();
69
- throw new InwinkApiError(401, 'Session expirée, reconnexion requise.');
72
+ const renewed = await renewToken();
73
+ if (renewed) {
74
+ token = renewed;
75
+ response = await doFetch(path, token, init);
76
+ }
77
+ if (response.status === 401) {
78
+ await login();
79
+ throw new InwinkApiError(401, 'Session expirée, reconnexion requise.');
80
+ }
70
81
  }
71
- }
72
82
 
73
- if (!response.ok) {
74
- throw new InwinkApiError(response.status, await safeBody(response));
75
- }
83
+ if (!response.ok) {
84
+ throw new InwinkApiError(response.status, await safeBody(response));
85
+ }
76
86
 
77
- if (response.status === 204) {
78
- return undefined as T;
87
+ if (response.status === 204) {
88
+ return undefined as T;
89
+ }
90
+ return (await response.json()) as T;
79
91
  }
80
- return (await response.json()) as T;
81
- }
82
92
 
83
- const withBody =
84
- (method: string) =>
85
- <T = unknown>(path: string, body?: unknown, init?: RequestInit): Promise<T> =>
86
- request<T>(path, {
87
- ...init,
88
- method,
89
- body: body === undefined ? init?.body : JSON.stringify(body),
90
- });
93
+ const withBody =
94
+ (method: string) =>
95
+ <T = unknown>(path: string, body?: unknown, init?: RequestInit): Promise<T> =>
96
+ request<T>(path, {
97
+ ...init,
98
+ method,
99
+ body: body === undefined ? init?.body : JSON.stringify(body),
100
+ });
91
101
 
92
- return {
93
- request,
94
- get: <T = unknown>(path: string, init?: RequestInit) =>
95
- request<T>(path, { ...init, method: 'GET' }),
96
- post: withBody('POST'),
97
- put: withBody('PUT'),
98
- del: <T = unknown>(path: string, init?: RequestInit) =>
99
- request<T>(path, { ...init, method: 'DELETE' }),
100
- };
102
+ return {
103
+ request,
104
+ get: <T = unknown>(path: string, init?: RequestInit) =>
105
+ request<T>(path, { ...init, method: 'GET' }),
106
+ post: withBody('POST'),
107
+ put: withBody('PUT'),
108
+ del: <T = unknown>(path: string, init?: RequestInit) =>
109
+ request<T>(path, { ...init, method: 'DELETE' }),
110
+ };
111
+ }, [baseUrl]);
101
112
  }
@@ -3,13 +3,16 @@ import ReactDOM from 'react-dom/client';
3
3
  import { HashRouter } from 'react-router-dom';
4
4
  import App from './App';
5
5
  import { AuthProvider } from './auth/AuthProvider';
6
+ import { InwinkContextProvider } from './context/InwinkContextProvider';
6
7
  import './styles.css';
7
8
 
8
9
  ReactDOM.createRoot(document.getElementById('root')!).render(
9
10
  <React.StrictMode>
10
11
  <HashRouter>
11
12
  <AuthProvider>
12
- <App />
13
+ <InwinkContextProvider>
14
+ <App />
15
+ </InwinkContextProvider>
13
16
  </AuthProvider>
14
17
  </HashRouter>
15
18
  </React.StrictMode>,
@@ -1,32 +1,91 @@
1
+ /* Polices inwink : Lato (texte) + Poppins (titres) via Google Fonts.
2
+ Nécessite que fonts.googleapis.com / fonts.gstatic.com soient déclarés
3
+ dans extension.manifest.json > permissions.externalHosts. */
4
+ @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&family=Poppins:wght@500;600;700&display=swap');
5
+
6
+ /* Design tokens — inspirés de l'identité inwink. */
1
7
  :root {
2
- font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
8
+ /* Polices */
9
+ --font-base: 'Lato', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
10
+ --font-title: 'Poppins', var(--font-base);
11
+
12
+ /* Couleur d'accentuation */
13
+ --color-accent: #e94c1e;
14
+ --color-accent-hover: #cf3f15;
15
+ --color-accent-contrast: #ffffff;
16
+
17
+ /* Neutres */
18
+ --color-bg: #ffffff;
19
+ --color-surface: #f7f7f8;
20
+ --color-border: #e2e2e6;
21
+ --color-text: #1d1d1f;
22
+ --color-text-muted: #6b6b73;
23
+
24
+ /* Rayons & ombres */
25
+ --radius: 8px;
26
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
27
+
28
+ font-family: var(--font-base);
3
29
  line-height: 1.5;
4
- color-scheme: light dark;
30
+ color: var(--color-text);
31
+ background: var(--color-bg);
32
+ }
33
+
34
+ * {
35
+ box-sizing: border-box;
5
36
  }
6
37
 
7
38
  body {
8
39
  margin: 0;
9
- padding: 2rem;
40
+ font-family: var(--font-base);
41
+ background: var(--color-bg);
42
+ color: var(--color-text);
10
43
  }
11
44
 
12
- main {
13
- max-width: 720px;
14
- margin: 0 auto;
45
+ h1,
46
+ h2,
47
+ h3 {
48
+ font-family: var(--font-title);
49
+ font-weight: 600;
50
+ line-height: 1.2;
15
51
  }
16
52
 
53
+ a {
54
+ color: var(--color-accent);
55
+ }
56
+
57
+ /* Boutons — base générique réutilisable.
58
+ .btn-primary = action principale (accent), sinon variante neutre. */
17
59
  button {
18
60
  cursor: pointer;
19
61
  display: inline-flex;
20
62
  align-items: center;
21
63
  gap: 0.5rem;
22
64
  padding: 0.5rem 1rem;
23
- border-radius: 6px;
24
- border: 1px solid currentColor;
25
- background: transparent;
65
+ border-radius: var(--radius);
66
+ border: 1px solid var(--color-border);
67
+ background: var(--color-bg);
68
+ color: var(--color-text);
26
69
  font: inherit;
70
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
71
+ }
72
+
73
+ button:hover:not(:disabled) {
74
+ border-color: var(--color-accent);
27
75
  }
28
76
 
29
77
  button:disabled {
30
78
  cursor: default;
31
79
  opacity: 0.6;
32
80
  }
81
+
82
+ button.btn-primary {
83
+ background: var(--color-accent);
84
+ border-color: var(--color-accent);
85
+ color: var(--color-accent-contrast);
86
+ }
87
+
88
+ button.btn-primary:hover:not(:disabled) {
89
+ background: var(--color-accent-hover);
90
+ border-color: var(--color-accent-hover);
91
+ }