shutro-erp 0.3.3 → 0.3.4
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/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +128 -1
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +128 -1
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +128 -1
- package/dist/integrations/index.js +12 -0
- package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
- package/dist/integrations/web/OAuthAppsPage.js +79 -0
- package/dist/integrations/web/index.d.ts +1 -0
- package/dist/integrations/web/index.js +3 -0
- package/dist/integrations/web/messages.js +58 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +128 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +128 -1
- package/dist/messaging/web/index.js +1 -1
- package/dist/notify/web/index.js +1 -1
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +128 -1
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +128 -1
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +128 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +128 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +128 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +128 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +128 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +128 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +128 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +128 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +128 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +128 -1
- package/dist/server/auth/base.js +76 -0
- package/dist/server/auth/oauth.d.ts +21 -0
- package/dist/server/auth/oauth.js +579 -0
- package/dist/server/auth/service.d.ts +4 -0
- package/dist/server/auth/service.js +17 -0
- package/dist/server/identity.d.ts +2 -0
- package/dist/server/images.d.ts +12 -0
- package/dist/server/images.js +139 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.js +9 -1
- package/dist/settings/company.js +3 -1
- package/dist/settings/people.js +3 -1
- package/dist/settings/web/CompaniesPage.js +2 -2
- package/dist/settings/web/CompanyPage.js +13 -2
- package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
- package/dist/settings/web/ConnectedAppsPage.js +33 -0
- package/dist/settings/web/OverviewPage.js +12 -1
- package/dist/settings/web/PeoplePage.js +3 -12
- package/dist/settings/web/ProfilePage.js +9 -7
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +5 -0
- package/dist/settings/web/messages.js +28 -0
- package/dist/ui/api.d.ts +4 -0
- package/dist/ui/app.js +49 -16
- package/dist/ui/components/AppIcon.js +11 -1
- package/dist/ui/components/Avatar.d.ts +21 -0
- package/dist/ui/components/Avatar.js +71 -0
- package/dist/ui/i18n/messages.d.ts +30 -0
- package/dist/ui/i18n/messages.js +60 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/registry.d.ts +7 -1
- package/dist/ui/registry.js +10 -1
- package/dist/ui/search/ViewTabs.d.ts +14 -0
- package/dist/ui/search/ViewTabs.js +42 -0
- package/dist/ui/shell/Consent.d.ts +5 -0
- package/dist/ui/shell/Consent.js +35 -0
- package/dist/ui/shell/Home.js +85 -16
- package/dist/ui/shell/Sidebar.js +15 -22
- package/dist/ui/views/ListView.js +49 -22
- package/package.json +1 -1
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/** Messages of the Settings app (namespace `settings`). */
|
|
2
2
|
export const messages = {
|
|
3
3
|
en: {
|
|
4
|
+
connected: {
|
|
5
|
+
title: 'Connected apps',
|
|
6
|
+
intro: 'Apps you allowed to act for you. They can never do more than you may; remove one and its access ends at once.',
|
|
7
|
+
none: 'You have not allowed any app.',
|
|
8
|
+
since: 'allowed {{when}}',
|
|
9
|
+
lastUsed: 'last used {{when}}',
|
|
10
|
+
neverUsed: 'not used yet',
|
|
11
|
+
revoke: 'Remove access',
|
|
12
|
+
revokeApp: 'Remove the access of {{app}}',
|
|
13
|
+
homepage: 'About {{app}}',
|
|
14
|
+
},
|
|
4
15
|
roles: {
|
|
5
16
|
title: 'Roles & permissions',
|
|
6
17
|
intro: 'What each role lets people open and do. Changes are previewed before they are saved.',
|
|
@@ -133,6 +144,10 @@ export const messages = {
|
|
|
133
144
|
description: 'Every company, its currency and people; switch between them or add one.',
|
|
134
145
|
keywords: 'companies multi-company branches new company switch',
|
|
135
146
|
},
|
|
147
|
+
'settings.action_people': {
|
|
148
|
+
description: 'Invite people, give them roles per app and company, deactivate them, reset their two-factor sign-in.',
|
|
149
|
+
keywords: 'people users employees invite invitation roles deactivate 2fa reset',
|
|
150
|
+
},
|
|
136
151
|
'settings.action_roles': {
|
|
137
152
|
description: 'What each role opens and allows, rights per model, record rules; previewed before saving.',
|
|
138
153
|
keywords: 'roles permissions access rights groups record rules field security acl',
|
|
@@ -184,6 +199,7 @@ export const messages = {
|
|
|
184
199
|
turnOff: 'Turn off two-factor sign-in',
|
|
185
200
|
password: 'Password',
|
|
186
201
|
recoveryCodes: 'Your recovery codes: each signs you in once if you lose your phone. Keep them somewhere safe; they are shown only now.',
|
|
202
|
+
photo: 'Photo',
|
|
187
203
|
sessions: 'Signed-in devices',
|
|
188
204
|
sessionsHint: 'Where you are signed in now. Sign out a device you do not recognise.',
|
|
189
205
|
thisDevice: 'This device',
|
|
@@ -368,6 +384,17 @@ export const messages = {
|
|
|
368
384
|
version: 'Version {{version}}',
|
|
369
385
|
},
|
|
370
386
|
bn: {
|
|
387
|
+
connected: {
|
|
388
|
+
title: 'সংযুক্ত অ্যাপস',
|
|
389
|
+
intro: 'যে অ্যাপগুলোকে আপনি আপনার হয়ে কাজ করতে দিয়েছেন। সরিয়ে দিলে সাথে সাথে প্রবেশাধিকার শেষ।',
|
|
390
|
+
none: 'আপনি কোনো অ্যাপকে অনুমতি দেননি।',
|
|
391
|
+
since: '{{when}} থেকে অনুমতি',
|
|
392
|
+
lastUsed: 'সর্বশেষ ব্যবহার {{when}}',
|
|
393
|
+
neverUsed: 'এখনো ব্যবহার হয়নি',
|
|
394
|
+
revoke: 'প্রবেশাধিকার সরান',
|
|
395
|
+
revokeApp: '{{app}}-এর প্রবেশাধিকার সরান',
|
|
396
|
+
homepage: '{{app}} সম্পর্কে',
|
|
397
|
+
},
|
|
371
398
|
roles: {
|
|
372
399
|
title: 'রোল ও অনুমতি',
|
|
373
400
|
intro: 'প্রতিটি রোল মানুষকে কী খুলতে ও করতে দেয়। সংরক্ষণের আগে পরিবর্তন দেখানো হয়।',
|
|
@@ -555,6 +582,7 @@ export const messages = {
|
|
|
555
582
|
turnOff: 'দুই-ধাপের সাইন ইন বন্ধ করুন',
|
|
556
583
|
password: 'পাসওয়ার্ড',
|
|
557
584
|
recoveryCodes: 'আপনার রিকভারি কোড: ফোন হারালে প্রতিটি একবার সাইন ইন করায়। নিরাপদে রাখুন; শুধু এখনই দেখানো হচ্ছে।',
|
|
585
|
+
photo: 'ছবি',
|
|
558
586
|
sessions: 'সাইন ইন করা ডিভাইস',
|
|
559
587
|
sessionsHint: 'এখন যেখানে আপনি সাইন ইন আছেন। অচেনা ডিভাইস সাইন আউট করুন।',
|
|
560
588
|
thisDevice: 'এই ডিভাইস',
|
package/dist/ui/api.d.ts
CHANGED
|
@@ -74,6 +74,10 @@ export interface CurrentUser {
|
|
|
74
74
|
readonly administratorNeedsSecondFactor: boolean;
|
|
75
75
|
/** The policy requires two-factor sign-in of everyone and it is not on (RFC 0019 §3.5); older servers send none. */
|
|
76
76
|
readonly twoFactorSetupRequired?: boolean;
|
|
77
|
+
/** The user's photo version, or null (older servers send none). */
|
|
78
|
+
readonly avatar?: string | null;
|
|
79
|
+
/** Each company's logo version, when it has one. */
|
|
80
|
+
readonly logos?: Readonly<Record<string, string>>;
|
|
77
81
|
}
|
|
78
82
|
export interface AppEntry {
|
|
79
83
|
readonly code: string;
|
package/dist/ui/app.js
CHANGED
|
@@ -12,11 +12,12 @@ import { compileSearch, decodeSearch, encodeSearch } from '../search/index.js';
|
|
|
12
12
|
import { onlineManager, QueryClient, QueryClientProvider, useQuery, useQueryClient, } from '@tanstack/react-query';
|
|
13
13
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
|
14
14
|
import { createRootRoute, createRoute, createRouter, Link, Navigate, Outlet, RouterProvider, useNavigate, useParams, useSearch, } from '@tanstack/react-router';
|
|
15
|
-
import { CalendarDays, ChartColumn, ChartGantt, ChevronRight, ChevronsUpDown, Columns3, Grid2x2, Languages, LayoutDashboard, List, LogOut, Map as MapIcon, Monitor, Moon, Network, PanelLeft, Plus, Search, Sheet, Sun, Table2, } from 'lucide-react';
|
|
15
|
+
import { CalendarDays, ChartColumn, ChartGantt, ChevronDown, ChevronRight, ChevronsUpDown, Columns3, Grid2x2, House, Languages, LayoutDashboard, List, LogOut, Map as MapIcon, Monitor, Moon, Network, PanelLeft, Plus, Search, Sheet, Sun, Table2, } from 'lucide-react';
|
|
16
16
|
import { Direction } from 'radix-ui';
|
|
17
17
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
|
|
18
18
|
import { I18nextProvider, useTranslation } from 'react-i18next';
|
|
19
19
|
import { ApiError } from './api.js';
|
|
20
|
+
import { Avatar, imageUrl } from './components/Avatar.js';
|
|
20
21
|
import { Alert, Button, Menu, MenuCheckboxItem, MenuContent, MenuItem, MenuLabel, MenuSeparator, MenuTrigger, Spinner, } from './components/primitives.js';
|
|
21
22
|
import { KoshProvider, useKosh } from './context.js';
|
|
22
23
|
import { DesignProvider } from './design.js';
|
|
@@ -29,8 +30,10 @@ import { setPrefill, takePrefill } from './prefill.js';
|
|
|
29
30
|
import { onAppUpdate, registerServiceWorker } from './pwa/register.js';
|
|
30
31
|
import { createRealtimeClient } from './realtime.js';
|
|
31
32
|
import { SearchBar } from './search/SearchBar.js';
|
|
33
|
+
import { ViewTabs } from './search/ViewTabs.js';
|
|
32
34
|
import { Activate } from './shell/Activate.js';
|
|
33
35
|
import { CommandPalette, PaletteContext } from './shell/CommandPalette.js';
|
|
36
|
+
import { Consent } from './shell/Consent.js';
|
|
34
37
|
import { Home } from './shell/Home.js';
|
|
35
38
|
import { Login } from './shell/Login.js';
|
|
36
39
|
import { OfflineMenu } from './shell/OfflineMenu.js';
|
|
@@ -142,7 +145,8 @@ function CompanyBlock({ me, company, collapsed, onChange, }) {
|
|
|
142
145
|
enabled: me.companies.length > 0,
|
|
143
146
|
});
|
|
144
147
|
const name = names.data?.find((c) => c.id === company)?.displayName ?? t('app.name');
|
|
145
|
-
|
|
148
|
+
// The company's logo, else its initial.
|
|
149
|
+
const badge = (_jsx(Avatar, { name: name.slice(0, 1), src: imageUrl('company', company, me.logos?.[String(company)]), shape: "square", className: "font-semibold" }));
|
|
146
150
|
if (collapsed)
|
|
147
151
|
return _jsx("span", { title: name, children: badge });
|
|
148
152
|
if (me.companies.length < 2)
|
|
@@ -153,19 +157,16 @@ function CompanyBlock({ me, company, collapsed, onChange, }) {
|
|
|
153
157
|
onChange(c.id);
|
|
154
158
|
}, children: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": true, className: "grid size-5 place-items-center rounded bg-muted text-[10px] font-semibold", children: c.displayName.slice(0, 1).toUpperCase() }), c.displayName] }) }, c.id)))] })] }));
|
|
155
159
|
}
|
|
156
|
-
|
|
157
|
-
function UserMenu({ user, collapsed, }) {
|
|
160
|
+
function UserMenu({ user, collapsed, corner, }) {
|
|
158
161
|
const { t } = useTranslation();
|
|
159
|
-
const { api, registry, persister } = useServices();
|
|
162
|
+
const { api, registry, persister, lang, setLang } = useServices();
|
|
163
|
+
const { theme, setTheme, density, setDensity } = useKosh();
|
|
160
164
|
const queryClient = useQueryClient();
|
|
161
165
|
const navigate = useNavigate();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
.slice(0, 2)
|
|
167
|
-
.toUpperCase();
|
|
168
|
-
return (_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": user.name, className: cn('flex w-full min-w-0 items-center gap-2 rounded-md px-1.5 py-1.5 text-sm hover:bg-muted', collapsed && 'justify-center'), children: [_jsx("span", { "aria-hidden": true, className: "grid size-7 shrink-0 place-items-center rounded-full bg-muted text-xs font-medium", children: initials }), collapsed ? null : _jsx("span", { className: "truncate", children: user.name })] }) }), _jsxs(MenuContent, { align: "start", side: "top", children: [_jsx(MenuLabel, { children: user.name }), registry.userPages().map((p) => (_jsx(MenuItem, { onSelect: () => void navigate({ to: '/me/$page', params: { page: p.name } }), children: t(p.label) }, p.name))), _jsxs(MenuItem, { onSelect: async () => {
|
|
166
|
+
return (_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": user.name, "data-kosh-account": corner === undefined ? 'sidebar' : 'corner', className: cn('flex min-w-0 items-center gap-2 rounded-md px-1.5 py-1.5 text-sm hover:bg-muted', corner === undefined ? 'w-full' : 'rounded-full border bg-surface pe-3', collapsed && 'justify-center'), children: [_jsx(Avatar, { name: user.name, src: imageUrl('user', user.id, user.avatar) }), collapsed ? null : _jsx("span", { className: "truncate", children: user.name }), corner === undefined ? null : (_jsx(ChevronDown, { className: "size-3.5 text-muted-foreground", "aria-hidden": true }))] }) }), _jsxs(MenuContent, { align: corner === undefined ? 'start' : 'end', side: corner === undefined ? 'top' : 'bottom', className: corner === undefined ? undefined : 'max-h-[80vh] w-64 overflow-y-auto', children: [_jsxs(MenuLabel, { children: [user.name, _jsx("span", { className: "block text-xs font-normal text-muted-foreground", children: user.login })] }), registry.userPages().map((p) => (_jsx(MenuItem, { onSelect: () => void navigate({ to: '/me/$page', params: { page: p.name } }), children: t(p.label) }, p.name))), corner === undefined ? null : (_jsxs(_Fragment, { children: [corner.companies.length < 2 ? null : (_jsxs(_Fragment, { children: [_jsx(MenuSeparator, {}), _jsx(MenuLabel, { children: t('shell.company') }), corner.companies.map((c) => (_jsx(MenuCheckboxItem, { checked: c.id === corner.current, onCheckedChange: () => {
|
|
167
|
+
if (c.id !== corner.current)
|
|
168
|
+
corner.onSwitch(c.id);
|
|
169
|
+
}, children: c.name }, c.id)))] })), _jsx(MenuSeparator, {}), _jsx(MenuLabel, { children: t('shell.language') }), LANGUAGES.map((l) => (_jsx(MenuCheckboxItem, { checked: lang === l.code, onCheckedChange: () => setLang(l.code), children: _jsx("span", { lang: l.code.replace('_', '-'), children: l.name }) }, l.code))), _jsx(MenuSeparator, {}), _jsx(MenuLabel, { children: t('shell.theme') }), ['light', 'dark', 'system'].map((choice) => (_jsx(MenuCheckboxItem, { checked: theme === choice, onCheckedChange: () => setTheme(choice), children: t(`shell.${choice}`) }, choice))), _jsx(MenuLabel, { children: t('shell.density') }), ['comfortable', 'compact'].map((choice) => (_jsx(MenuCheckboxItem, { checked: density === choice, onCheckedChange: () => setDensity(choice), children: t(`views.${choice}`) }, choice)))] })), _jsx(MenuSeparator, {}), _jsxs(MenuItem, { onSelect: async () => {
|
|
169
170
|
await api.logout();
|
|
170
171
|
queryClient.clear();
|
|
171
172
|
await dropOfflineCache(persister);
|
|
@@ -247,11 +248,21 @@ function Shell() {
|
|
|
247
248
|
setDrawer(false);
|
|
248
249
|
setPalette(true);
|
|
249
250
|
}, top: _jsx(CompanyBlock, { me: user, company: current, collapsed: inDrawer ? false : collapsed, onChange: switchCompany }), bottom: _jsx(UserMenu, { user: user, collapsed: inDrawer ? false : collapsed }), onNavigate: inDrawer ? () => setDrawer(false) : undefined }));
|
|
250
|
-
return (_jsx(KoshProvider, { api: api, registry: registry, uid: user.id, companyId: current, lang: lang, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, grouping: user.groupings?.[String(current)] ?? 'international', accent: user.accents?.[String(current)] ?? null, ...(realtime === undefined ? {} : { realtime }), ...(offline === undefined ? {} : { offline }), switchCompany: switchCompany, children: _jsx(ToastProvider, { children: _jsxs(DesignProvider, { children: [_jsx(OfflineAgent, { uid: user.id }), _jsxs(PaletteContext.Provider, { value: openPalette, children: [_jsx("a", { href: "#content", className: "sr-only focus:not-sr-only focus:absolute focus:start-2 focus:top-2 focus:z-50 focus:rounded focus:bg-surface focus:px-3 focus:py-2", children: t('shell.skip') }), _jsxs("div", { className: "flex min-h-screen", children: [
|
|
251
|
+
return (_jsx(KoshProvider, { api: api, registry: registry, uid: user.id, companyId: current, lang: lang, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, grouping: user.groupings?.[String(current)] ?? 'international', accent: user.accents?.[String(current)] ?? null, ...(realtime === undefined ? {} : { realtime }), ...(offline === undefined ? {} : { offline }), switchCompany: switchCompany, children: _jsx(ToastProvider, { children: _jsxs(DesignProvider, { children: [_jsx(OfflineAgent, { uid: user.id }), _jsxs(PaletteContext.Provider, { value: openPalette, children: [_jsx("a", { href: "#content", className: "sr-only focus:not-sr-only focus:absolute focus:start-2 focus:top-2 focus:z-50 focus:rounded focus:bg-surface focus:px-3 focus:py-2", children: t('shell.skip') }), params.app === undefined ? (_jsxs("div", { className: "flex min-h-screen flex-col", children: [_jsxs("div", { className: "flex h-14 items-center gap-2 px-4 lg:px-6", children: [userPage === undefined ? null : (_jsxs(Link, { to: "/", className: "flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(House, { className: "size-4", "aria-hidden": true }), t('shell.home')] })), _jsx("span", { className: "flex-1" }), registry
|
|
252
|
+
.headerItems()
|
|
253
|
+
.filter((item) => item.home)
|
|
254
|
+
.map(({ name, component: Item }) => (_jsx(Item, { api: api }, name))), _jsx(UserMenu, { user: user, collapsed: false, corner: {
|
|
255
|
+
companies: (companyNames.data ?? []).map((c) => ({
|
|
256
|
+
id: c.id,
|
|
257
|
+
name: c.displayName,
|
|
258
|
+
})),
|
|
259
|
+
current,
|
|
260
|
+
onSwitch: switchCompany,
|
|
261
|
+
} })] }), user.administratorNeedsSecondFactor ? (_jsx("div", { className: "px-4 pt-3", children: _jsx(Alert, { tone: "info", children: t('auth.adminNeeds2fa') }) })) : null, _jsx("main", { id: "content", className: "mx-auto w-full max-w-[100rem] flex-1 p-4 lg:p-6", children: _jsx(Outlet, {}) })] })) : (_jsxs("div", { className: "flex min-h-screen", children: [_jsx("aside", { "aria-label": t('shell.sidebar'), className: cn('sticky top-0 hidden h-screen shrink-0 border-e bg-surface transition-[width] lg:block', collapsed ? 'w-14' : 'w-60'), children: sidebar(false) }), _jsx(SidebarDrawer, { open: drawer, onOpenChange: setDrawer, children: sidebar(true) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("header", { className: "sticky top-0 z-30 border-b bg-surface", children: _jsxs("div", { className: "flex h-12 items-center gap-2 px-3 lg:px-4", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden", "aria-label": t('shell.openMenu'), onClick: () => setDrawer(true), children: _jsx(PanelLeft, { className: "rtl:rotate-180", "aria-hidden": true }) }), _jsx("nav", { "aria-label": t('shell.location'), className: "flex min-w-0 flex-1 items-center gap-1.5 text-sm", children: app === undefined ? (_jsx("span", { className: "font-semibold", children: userPage !== undefined
|
|
251
262
|
? t(userPage.label)
|
|
252
263
|
: params.app === undefined
|
|
253
264
|
? t('shell.home')
|
|
254
|
-
: t('app.name') })) : (_jsxs(_Fragment, { children: [_jsx(Link, { to: "/$app", params: { app: app.code }, className: "font-semibold hover:underline", children: app.label }), section === undefined ? null : (_jsxs(_Fragment, { children: [_jsx(ChevronRight, { className: "size-3.5 text-muted-foreground rtl:rotate-180", "aria-hidden": true }), _jsx(Link, { to: "/$app/a/$action", params: { app: app.code, action: params.action ?? '' }, className: "truncate text-muted-foreground hover:text-foreground hover:underline", children: section })] }))] })) }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden", "aria-label": t('palette.button'), onClick: () => setPalette(true), children: _jsx(Search, { "aria-hidden": true }) }), _jsx(OfflineMenu, {}), registry.headerItems().map(({ name, component: Item }) => (_jsx(Item, { api: api }, name))), _jsx(LanguageMenu, {}), _jsx(ThemeMenu, {})] }) }), user.administratorNeedsSecondFactor ? (_jsx("div", { className: "px-4 pt-3", children: _jsx(Alert, { tone: "info", children: t('auth.adminNeeds2fa') }) })) : null, _jsx("main", { id: "content", className: "mx-auto w-full max-w-[100rem] flex-1 p-4 lg:p-6", children: _jsx(Outlet, {}) })] })] }), _jsx(CommandPalette, { open: palette, onOpenChange: setPalette, api: api, onShortcuts: openHelp, companies: (companyNames.data ?? [])
|
|
265
|
+
: t('app.name') })) : (_jsxs(_Fragment, { children: [_jsx(Link, { to: "/$app", params: { app: app.code }, className: "font-semibold hover:underline", children: app.label }), section === undefined ? null : (_jsxs(_Fragment, { children: [_jsx(ChevronRight, { className: "size-3.5 text-muted-foreground rtl:rotate-180", "aria-hidden": true }), _jsx(Link, { to: "/$app/a/$action", params: { app: app.code, action: params.action ?? '' }, className: "truncate text-muted-foreground hover:text-foreground hover:underline", children: section })] }))] })) }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden", "aria-label": t('palette.button'), onClick: () => setPalette(true), children: _jsx(Search, { "aria-hidden": true }) }), _jsx(OfflineMenu, {}), registry.headerItems().map(({ name, component: Item }) => (_jsx(Item, { api: api }, name))), _jsx(LanguageMenu, {}), _jsx(ThemeMenu, {})] }) }), user.administratorNeedsSecondFactor ? (_jsx("div", { className: "px-4 pt-3", children: _jsx(Alert, { tone: "info", children: t('auth.adminNeeds2fa') }) })) : null, _jsx("main", { id: "content", className: "mx-auto w-full max-w-[100rem] flex-1 p-4 lg:p-6", children: _jsx(Outlet, {}) })] })] })), _jsx(CommandPalette, { open: palette, onOpenChange: setPalette, api: api, onShortcuts: openHelp, companies: (companyNames.data ?? [])
|
|
255
266
|
.filter((c) => c.id !== current)
|
|
256
267
|
.map((c) => ({ id: c.id, name: c.displayName })), onSwitchCompany: switchCompany }), _jsx(ShortcutsHelp, { open: help, onOpenChange: setHelp })] })] }) }) }));
|
|
257
268
|
}
|
|
@@ -390,7 +401,7 @@ function WindowActionPage() {
|
|
|
390
401
|
params: { app, action: actionId },
|
|
391
402
|
search: { ...search, view: v },
|
|
392
403
|
}), children: _jsx(Icon, { "aria-hidden": true }) }, v));
|
|
393
|
-
})] })) : null] }), _jsx(SearchBar, { model: model, viewType: type, spec: spec.data, state: state, onChange: setState, labels: labels }), Renderer === undefined ? (_jsx(Alert, { children: `No renderer for '${served.data.view.component ?? type}'.` })) : (_jsx(Renderer, { served: served.data, where: compiled.where, groupBy: compiled.groupBy, order: compiled.order, onOrder: (order) => setState({ ...state, order }), onOpen: open, onDrill: drill }))] }));
|
|
404
|
+
})] })) : null] }), _jsx(ViewTabs, { model: model, viewType: type, state: state, onChange: setState }), _jsx(SearchBar, { model: model, viewType: type, spec: spec.data, state: state, onChange: setState, labels: labels }), Renderer === undefined ? (_jsx(Alert, { children: `No renderer for '${served.data.view.component ?? type}'.` })) : (_jsx(Renderer, { served: served.data, where: compiled.where, groupBy: compiled.groupBy, order: compiled.order, onOrder: (order) => setState({ ...state, order }), onOpen: open, onDrill: drill }))] }));
|
|
394
405
|
}
|
|
395
406
|
/** What the record page needs from the shell: the API, the user, the apps (for breadcrumbs). */
|
|
396
407
|
function useRecordContext() {
|
|
@@ -481,9 +492,24 @@ function ActivatePage() {
|
|
|
481
492
|
const navigate = useNavigate();
|
|
482
493
|
return _jsx(Activate, { api: useServices().api, onSignedIn: () => void navigate({ to: '/' }) });
|
|
483
494
|
}
|
|
495
|
+
/** Where to go after signing in: `?next=` when it is a path of this site (never another site), else home. */
|
|
496
|
+
function nextPath() {
|
|
497
|
+
const next = new URLSearchParams(window.location.search).get('next') ?? '';
|
|
498
|
+
return next.startsWith('/') && !next.startsWith('//') && !next.startsWith('/\\') ? next : '/';
|
|
499
|
+
}
|
|
484
500
|
function LoginPage() {
|
|
485
501
|
const navigate = useNavigate();
|
|
486
|
-
return _jsx(Login, { api: useServices().api, onSignedIn: () =>
|
|
502
|
+
return (_jsx(Login, { api: useServices().api, onSignedIn: () => {
|
|
503
|
+
const next = nextPath();
|
|
504
|
+
// A page outside the router's own paths with its query (the OAuth consent) loads as an address.
|
|
505
|
+
if (next.startsWith('/oauth/'))
|
|
506
|
+
window.location.assign(next);
|
|
507
|
+
else
|
|
508
|
+
void navigate({ to: next });
|
|
509
|
+
} }));
|
|
510
|
+
}
|
|
511
|
+
function ConsentPage() {
|
|
512
|
+
return _jsx(Consent, { api: useServices().api });
|
|
487
513
|
}
|
|
488
514
|
// ------------------------------------------------------------------------------------------------- router
|
|
489
515
|
const rootRoute = createRootRoute({ component: Outlet });
|
|
@@ -497,6 +523,12 @@ const activateRoute = createRoute({
|
|
|
497
523
|
path: '/activate',
|
|
498
524
|
component: ActivatePage,
|
|
499
525
|
});
|
|
526
|
+
/** The OAuth consent page (RFC 0017 §5.2): outside the shell, like sign-in. */
|
|
527
|
+
const consentRoute = createRoute({
|
|
528
|
+
getParentRoute: () => rootRoute,
|
|
529
|
+
path: '/oauth/authorize',
|
|
530
|
+
component: ConsentPage,
|
|
531
|
+
});
|
|
500
532
|
const shellRoute = createRoute({ getParentRoute: () => rootRoute, id: 'shell', component: Shell });
|
|
501
533
|
const indexRoute = createRoute({
|
|
502
534
|
getParentRoute: () => shellRoute,
|
|
@@ -546,6 +578,7 @@ const userPageRoute = createRoute({
|
|
|
546
578
|
const routeTree = rootRoute.addChildren([
|
|
547
579
|
loginRoute,
|
|
548
580
|
activateRoute,
|
|
581
|
+
consentRoute,
|
|
549
582
|
shellRoute.addChildren([
|
|
550
583
|
indexRoute,
|
|
551
584
|
openRoute,
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
* App icons by name (`app.icon` in a module manifest): the launcher's tiles and the App manager's cards. Names
|
|
4
4
|
* follow Lucide; an unknown name shows a neutral grid, never a broken image.
|
|
5
5
|
*/
|
|
6
|
-
import { BookOpen, Boxes, Briefcase, ChartColumn, FolderKanban, LayoutGrid, Mail, MessageSquare, MessagesSquare, Package, PencilRuler, Receipt, Ruler, Settings, ShieldCheck, ShoppingCart, Users, Wallet, Warehouse, Workflow, } from 'lucide-react';
|
|
6
|
+
import { BookOpen, Boxes, Briefcase, Building, Building2, ChartColumn, FolderKanban, LayoutGrid, LockKeyhole, Mail, MessageSquare, MessagesSquare, Package, PencilRuler, Plug, Receipt, Ruler, ScrollText, Settings, ShieldCheck, ShoppingCart, Sparkles, Users, Wallet, Warehouse, Workflow, } from 'lucide-react';
|
|
7
7
|
import { cn } from '../lib/cn.js';
|
|
8
8
|
const ICONS = {
|
|
9
9
|
settings: Settings,
|
|
@@ -37,6 +37,16 @@ const ICONS = {
|
|
|
37
37
|
shield: ShieldCheck,
|
|
38
38
|
permissions: ShieldCheck,
|
|
39
39
|
briefcase: Briefcase,
|
|
40
|
+
plug: Plug,
|
|
41
|
+
integrations: Plug,
|
|
42
|
+
cart: ShoppingCart,
|
|
43
|
+
company: Building2,
|
|
44
|
+
companies: Building,
|
|
45
|
+
security: LockKeyhole,
|
|
46
|
+
audit: ScrollText,
|
|
47
|
+
apps: LayoutGrid,
|
|
48
|
+
assistant: Sparkles,
|
|
49
|
+
sparkles: Sparkles,
|
|
40
50
|
};
|
|
41
51
|
/** A tinted square with the app's icon (decorative: the app's name is always shown next to it). */
|
|
42
52
|
export function AppIcon({ name, className, }) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
/** The URL of a photo (`user`) or logo (`company`), or null when there is none. */
|
|
3
|
+
export declare function imageUrl(kind: 'user' | 'company', id: number, version: string | null | undefined): string | null;
|
|
4
|
+
/** Up to two initials of a name. */
|
|
5
|
+
export declare function initialsOf(name: string): string;
|
|
6
|
+
export declare function Avatar({ name, src, shape, className, }: {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly src: string | null;
|
|
9
|
+
/** People are round; companies square. */
|
|
10
|
+
readonly shape?: 'circle' | 'square';
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
}): ReactElement;
|
|
13
|
+
export declare function ImagePicker({ name, src, fit, onChoose, onRemove, busy, }: {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly src: string | null;
|
|
16
|
+
readonly fit: 'photo' | 'logo';
|
|
17
|
+
/** The image as a `data:` URL, already small. */
|
|
18
|
+
readonly onChoose: (dataUrl: string) => void;
|
|
19
|
+
readonly onRemove: () => void;
|
|
20
|
+
readonly busy?: boolean;
|
|
21
|
+
}): ReactElement;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Photos and logos: `Avatar` shows a person's photo or a company's logo (the server's image route, versioned so the
|
|
4
|
+
* browser caches it), else the initials on a tint. `ImagePicker` chooses a file and makes it small in the browser —
|
|
5
|
+
* a 256 px square, cropped to the centre for photos, fitted whole for logos — before anything is sent.
|
|
6
|
+
*/
|
|
7
|
+
import { ImageUp, Trash2 } from 'lucide-react';
|
|
8
|
+
import { useId, useRef, useState } from 'react';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import { cn } from '../lib/cn.js';
|
|
11
|
+
import { Button } from './primitives.js';
|
|
12
|
+
/** The URL of a photo (`user`) or logo (`company`), or null when there is none. */
|
|
13
|
+
export function imageUrl(kind, id, version) {
|
|
14
|
+
return version === null || version === undefined
|
|
15
|
+
? null
|
|
16
|
+
: `/api/v1/images/${kind}/${id}?v=${version}`;
|
|
17
|
+
}
|
|
18
|
+
/** Up to two initials of a name. */
|
|
19
|
+
export function initialsOf(name) {
|
|
20
|
+
return name
|
|
21
|
+
.split(/\s+/)
|
|
22
|
+
.filter((w) => w !== '')
|
|
23
|
+
.map((w) => w.slice(0, 1))
|
|
24
|
+
.join('')
|
|
25
|
+
.slice(0, 2)
|
|
26
|
+
.toUpperCase();
|
|
27
|
+
}
|
|
28
|
+
export function Avatar({ name, src, shape = 'circle', className, }) {
|
|
29
|
+
const [failed, setFailed] = useState(false);
|
|
30
|
+
const round = shape === 'circle' ? 'rounded-full' : 'rounded-md';
|
|
31
|
+
if (src !== null && !failed)
|
|
32
|
+
return (_jsx("img", { src: src, alt: "", "aria-hidden": true, onError: () => setFailed(true), className: cn('size-7 shrink-0 bg-surface object-cover', shape === 'square' && 'object-contain', round, className) }));
|
|
33
|
+
return (_jsx("span", { "aria-hidden": true, className: cn('grid size-7 shrink-0 place-items-center bg-primary text-xs font-medium text-primary-foreground', round, className), children: initialsOf(name) }));
|
|
34
|
+
}
|
|
35
|
+
const SIZE = 256;
|
|
36
|
+
/** Draws `file` on a 256 px square: covered and centred (`photo`) or fitted whole (`logo`). */
|
|
37
|
+
async function shrink(file, fit) {
|
|
38
|
+
const bitmap = await createImageBitmap(file);
|
|
39
|
+
const canvas = document.createElement('canvas');
|
|
40
|
+
canvas.width = SIZE;
|
|
41
|
+
canvas.height = SIZE;
|
|
42
|
+
const ctx = canvas.getContext('2d');
|
|
43
|
+
if (ctx === null)
|
|
44
|
+
throw new Error('No canvas');
|
|
45
|
+
const scale = fit === 'photo'
|
|
46
|
+
? Math.max(SIZE / bitmap.width, SIZE / bitmap.height)
|
|
47
|
+
: Math.min(SIZE / bitmap.width, SIZE / bitmap.height);
|
|
48
|
+
const w = bitmap.width * scale;
|
|
49
|
+
const h = bitmap.height * scale;
|
|
50
|
+
ctx.imageSmoothingQuality = 'high';
|
|
51
|
+
ctx.drawImage(bitmap, (SIZE - w) / 2, (SIZE - h) / 2, w, h);
|
|
52
|
+
bitmap.close();
|
|
53
|
+
// Photos as WebP (small); logos as PNG (they keep transparency and sharp edges).
|
|
54
|
+
return fit === 'photo' ? canvas.toDataURL('image/webp', 0.86) : canvas.toDataURL('image/png');
|
|
55
|
+
}
|
|
56
|
+
export function ImagePicker({ name, src, fit, onChoose, onRemove, busy, }) {
|
|
57
|
+
const { t } = useTranslation();
|
|
58
|
+
const id = useId();
|
|
59
|
+
const input = useRef(null);
|
|
60
|
+
const [error, setError] = useState(null);
|
|
61
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx(Avatar, { name: name, src: src, shape: fit === 'photo' ? 'circle' : 'square', className: "size-16 text-lg" }), _jsxs("div", { className: "grid gap-1.5", children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx("input", { ref: input, id: id, type: "file", accept: "image/png,image/jpeg,image/webp", className: "sr-only", onChange: (e) => {
|
|
62
|
+
const file = e.target.files?.[0];
|
|
63
|
+
e.target.value = '';
|
|
64
|
+
if (file === undefined)
|
|
65
|
+
return;
|
|
66
|
+
setError(null);
|
|
67
|
+
shrink(file, fit)
|
|
68
|
+
.then(onChoose)
|
|
69
|
+
.catch(() => setError(t('image.unreadable')));
|
|
70
|
+
} }), _jsxs(Button, { size: "sm", busy: busy === true, onClick: () => input.current?.click(), children: [_jsx(ImageUp, { "aria-hidden": true }), fit === 'photo' ? t('image.choosePhoto') : t('image.chooseLogo')] }), src === null ? null : (_jsxs(Button, { size: "sm", variant: "ghost", onClick: onRemove, children: [_jsx(Trash2, { "aria-hidden": true }), t('image.remove')] }))] }), _jsx("p", { className: "text-xs text-muted-foreground", children: error ?? t('image.hint') })] })] }));
|
|
71
|
+
}
|
|
@@ -113,6 +113,7 @@ export declare const en: {
|
|
|
113
113
|
compact: string;
|
|
114
114
|
comfortable: string;
|
|
115
115
|
emptyTitle: string;
|
|
116
|
+
emptyNamed: string;
|
|
116
117
|
emptyHint: string;
|
|
117
118
|
emptyFiltered: string;
|
|
118
119
|
moved: string;
|
|
@@ -234,11 +235,35 @@ export declare const en: {
|
|
|
234
235
|
done: string;
|
|
235
236
|
recent: string;
|
|
236
237
|
apps: string;
|
|
238
|
+
today: string;
|
|
239
|
+
approve: string;
|
|
240
|
+
review: string;
|
|
241
|
+
dueToday: string;
|
|
242
|
+
mentioned: string;
|
|
243
|
+
saved: string;
|
|
237
244
|
};
|
|
238
245
|
toast: {
|
|
239
246
|
region: string;
|
|
240
247
|
close: string;
|
|
241
248
|
};
|
|
249
|
+
image: {
|
|
250
|
+
choosePhoto: string;
|
|
251
|
+
chooseLogo: string;
|
|
252
|
+
remove: string;
|
|
253
|
+
hint: string;
|
|
254
|
+
unreadable: string;
|
|
255
|
+
};
|
|
256
|
+
oauth: {
|
|
257
|
+
title: string;
|
|
258
|
+
as: string;
|
|
259
|
+
may: string;
|
|
260
|
+
limits: string;
|
|
261
|
+
back: string;
|
|
262
|
+
homepage: string;
|
|
263
|
+
already: string;
|
|
264
|
+
allow: string;
|
|
265
|
+
cancel: string;
|
|
266
|
+
};
|
|
242
267
|
palette: {
|
|
243
268
|
switchCompany: string;
|
|
244
269
|
profile: string;
|
|
@@ -292,6 +317,11 @@ export declare const en: {
|
|
|
292
317
|
custom: string;
|
|
293
318
|
remove: string;
|
|
294
319
|
or: string;
|
|
320
|
+
savedViews: string;
|
|
321
|
+
allRecords: string;
|
|
322
|
+
moreViews_one: string;
|
|
323
|
+
moreViews_other: string;
|
|
324
|
+
shared: string;
|
|
295
325
|
};
|
|
296
326
|
};
|
|
297
327
|
type Messages = typeof en;
|
package/dist/ui/i18n/messages.js
CHANGED
|
@@ -110,6 +110,7 @@ export const en = {
|
|
|
110
110
|
compact: 'Compact',
|
|
111
111
|
comfortable: 'Comfortable',
|
|
112
112
|
emptyTitle: 'Nothing here yet',
|
|
113
|
+
emptyNamed: 'No {{name}} yet',
|
|
113
114
|
emptyHint: 'Records matching the current search will appear here.',
|
|
114
115
|
emptyFiltered: 'No record matches the current search.',
|
|
115
116
|
moved: '{{name}} moved to {{column}}',
|
|
@@ -231,8 +232,32 @@ export const en = {
|
|
|
231
232
|
done: 'Mark done',
|
|
232
233
|
recent: 'Recently opened',
|
|
233
234
|
apps: 'Apps',
|
|
235
|
+
today: 'Today, {{date}}',
|
|
236
|
+
approve: 'Approve {{record}}',
|
|
237
|
+
review: 'Review',
|
|
238
|
+
dueToday: 'due today',
|
|
239
|
+
mentioned: '{{actor}} mentioned you on {{record}}',
|
|
240
|
+
saved: 'Your saved views',
|
|
234
241
|
},
|
|
235
242
|
toast: { region: 'Notifications', close: 'Close' },
|
|
243
|
+
image: {
|
|
244
|
+
choosePhoto: 'Change photo',
|
|
245
|
+
chooseLogo: 'Change logo',
|
|
246
|
+
remove: 'Remove',
|
|
247
|
+
hint: 'PNG, JPEG or WebP. It is made small (256 px) before it is sent.',
|
|
248
|
+
unreadable: 'This file is not an image the browser can read.',
|
|
249
|
+
},
|
|
250
|
+
oauth: {
|
|
251
|
+
title: '{{app}} wants to use your account',
|
|
252
|
+
as: 'Signed in as {{name}} ({{login}}).',
|
|
253
|
+
may: 'It will be able to:',
|
|
254
|
+
limits: 'Never more than you may do yourself. You can take this back at any time in Connected apps.',
|
|
255
|
+
back: 'You will go back to {{host}}.',
|
|
256
|
+
homepage: 'About the app',
|
|
257
|
+
already: 'You allowed this before.',
|
|
258
|
+
allow: 'Allow',
|
|
259
|
+
cancel: 'Cancel',
|
|
260
|
+
},
|
|
236
261
|
palette: {
|
|
237
262
|
switchCompany: 'Switch to {{name}}',
|
|
238
263
|
profile: 'Your profile',
|
|
@@ -281,6 +306,11 @@ export const en = {
|
|
|
281
306
|
custom: 'Custom filter',
|
|
282
307
|
remove: 'Remove {{facet}}',
|
|
283
308
|
or: 'or',
|
|
309
|
+
savedViews: 'Saved views',
|
|
310
|
+
allRecords: 'All',
|
|
311
|
+
moreViews_one: '{{count}} more',
|
|
312
|
+
moreViews_other: '{{count}} more',
|
|
313
|
+
shared: 'Shared',
|
|
284
314
|
},
|
|
285
315
|
};
|
|
286
316
|
export const bn = {
|
|
@@ -391,6 +421,7 @@ export const bn = {
|
|
|
391
421
|
compact: 'সংক্ষিপ্ত',
|
|
392
422
|
comfortable: 'স্বাভাবিক',
|
|
393
423
|
emptyTitle: 'এখানে এখনো কিছু নেই',
|
|
424
|
+
emptyNamed: 'এখনো কোনো {{name}} নেই',
|
|
394
425
|
emptyHint: 'বর্তমান খোঁজের সাথে মেলা রেকর্ড এখানে দেখা যাবে।',
|
|
395
426
|
emptyFiltered: 'বর্তমান খোঁজের সাথে কোনো রেকর্ড মেলেনি।',
|
|
396
427
|
moved: '{{name}} {{column}}-এ সরানো হয়েছে',
|
|
@@ -509,8 +540,32 @@ export const bn = {
|
|
|
509
540
|
done: 'সম্পন্ন',
|
|
510
541
|
recent: 'সম্প্রতি খোলা',
|
|
511
542
|
apps: 'অ্যাপস',
|
|
543
|
+
today: 'আজ, {{date}}',
|
|
544
|
+
approve: '{{record}} অনুমোদন করুন',
|
|
545
|
+
review: 'দেখুন',
|
|
546
|
+
dueToday: 'আজকের',
|
|
547
|
+
mentioned: '{{actor}} আপনাকে {{record}}-এ উল্লেখ করেছেন',
|
|
548
|
+
saved: 'আপনার সংরক্ষিত ভিউ',
|
|
512
549
|
},
|
|
513
550
|
toast: { region: 'বিজ্ঞপ্তি', close: 'বন্ধ করুন' },
|
|
551
|
+
image: {
|
|
552
|
+
choosePhoto: 'ছবি বদলান',
|
|
553
|
+
chooseLogo: 'লোগো বদলান',
|
|
554
|
+
remove: 'সরান',
|
|
555
|
+
hint: 'PNG, JPEG বা WebP। পাঠানোর আগে ছোট (২৫৬ px) করা হয়।',
|
|
556
|
+
unreadable: 'এই ফাইলটি ব্রাউজার পড়তে পারে এমন ছবি নয়।',
|
|
557
|
+
},
|
|
558
|
+
oauth: {
|
|
559
|
+
title: '{{app}} আপনার অ্যাকাউন্ট ব্যবহার করতে চায়',
|
|
560
|
+
as: '{{name}} ({{login}}) হিসেবে সাইন ইন করা।',
|
|
561
|
+
may: 'এটি যা করতে পারবে:',
|
|
562
|
+
limits: 'আপনি নিজে যা পারেন তার বেশি কখনো নয়। যেকোনো সময় সংযুক্ত অ্যাপস থেকে ফিরিয়ে নিতে পারেন।',
|
|
563
|
+
back: 'আপনি {{host}}-এ ফিরে যাবেন।',
|
|
564
|
+
homepage: 'অ্যাপ সম্পর্কে',
|
|
565
|
+
already: 'আপনি আগে এটি অনুমতি দিয়েছিলেন।',
|
|
566
|
+
allow: 'অনুমতি দিন',
|
|
567
|
+
cancel: 'বাতিল',
|
|
568
|
+
},
|
|
514
569
|
palette: {
|
|
515
570
|
switchCompany: '{{name}}-এ যান',
|
|
516
571
|
profile: 'আপনার প্রোফাইল',
|
|
@@ -559,6 +614,11 @@ export const bn = {
|
|
|
559
614
|
custom: 'নিজস্ব ফিল্টার',
|
|
560
615
|
remove: '{{facet}} সরান',
|
|
561
616
|
or: 'অথবা',
|
|
617
|
+
savedViews: 'সংরক্ষিত ভিউ',
|
|
618
|
+
allRecords: 'সব',
|
|
619
|
+
moreViews_one: 'আরও {{count}}টি',
|
|
620
|
+
moreViews_other: 'আরও {{count}}টি',
|
|
621
|
+
shared: 'শেয়ার করা',
|
|
562
622
|
},
|
|
563
623
|
};
|
|
564
624
|
export const ar = {
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { Registry } from './registry.js';
|
|
|
10
10
|
export { ApiError, type ApiOptions, type AppEntry, type CurrentUser, createKoshApi, type KoshApi, type ListQuery, type MenuNode, NetworkError, type Problem, type Row, type ServedView, type Versioned, type ViewField, } from './api.js';
|
|
11
11
|
export { KoshApp, type KoshAppProps, useLanguage } from './app.js';
|
|
12
12
|
export { AppIcon } from './components/AppIcon.js';
|
|
13
|
+
export * from './components/Avatar.js';
|
|
13
14
|
export * from './components/primitives.js';
|
|
14
15
|
export { type KoshContextValue, KoshProvider, type ThemeChoice, useKosh, useTheme, } from './context.js';
|
|
15
16
|
export { type DesignContextValue, DesignProvider, type DesignSpot, useDesign, } from './design.js';
|
package/dist/ui/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { registerDefaultWidgets } from './views/widgets.js';
|
|
|
14
14
|
export { ApiError, createKoshApi, NetworkError, } from './api.js';
|
|
15
15
|
export { KoshApp, useLanguage } from './app.js';
|
|
16
16
|
export { AppIcon } from './components/AppIcon.js';
|
|
17
|
+
export * from './components/Avatar.js';
|
|
17
18
|
export * from './components/primitives.js';
|
|
18
19
|
export { KoshProvider, useKosh, useTheme, } from './context.js';
|
|
19
20
|
export { DesignProvider, useDesign, } from './design.js';
|
package/dist/ui/registry.d.ts
CHANGED
|
@@ -124,9 +124,13 @@ export declare class Registry {
|
|
|
124
124
|
/** The widget for a node: its `widget`, else the field kind, else plain text. */
|
|
125
125
|
widget(node: FieldNode, field: ViewField): ComponentType<WidgetProps>;
|
|
126
126
|
component(name: string): ComponentType<NodeComponentProps> | undefined;
|
|
127
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* An item of the shell's header, before the language and theme menus (`notify.Bell`). With `home`, it is also in
|
|
129
|
+
* the home page's corner, next to the account menu (notifications, messages).
|
|
130
|
+
*/
|
|
128
131
|
registerHeaderItem(name: string, component: ComponentType<HeaderItemProps>, options?: {
|
|
129
132
|
readonly replace?: boolean;
|
|
133
|
+
readonly home?: boolean;
|
|
130
134
|
}): this;
|
|
131
135
|
/**
|
|
132
136
|
* A personal page every signed-in user reaches from the user menu, outside any app (`settings.ApiKeys`):
|
|
@@ -147,6 +151,8 @@ export declare class Registry {
|
|
|
147
151
|
headerItems(): readonly {
|
|
148
152
|
readonly name: string;
|
|
149
153
|
readonly component: ComponentType<HeaderItemProps>;
|
|
154
|
+
/** Also on the home page. */
|
|
155
|
+
readonly home: boolean;
|
|
150
156
|
}[];
|
|
151
157
|
/** A control next to every search box (the AI assistant's "Ask"). */
|
|
152
158
|
registerSearchAssist(name: string, component: ComponentType<SearchAssistProps>, options?: {
|
package/dist/ui/registry.js
CHANGED
|
@@ -55,8 +55,16 @@ export class Registry {
|
|
|
55
55
|
component(name) {
|
|
56
56
|
return this.#entries.component.get(name);
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
#homeItems = new Set();
|
|
59
|
+
/**
|
|
60
|
+
* An item of the shell's header, before the language and theme menus (`notify.Bell`). With `home`, it is also in
|
|
61
|
+
* the home page's corner, next to the account menu (notifications, messages).
|
|
62
|
+
*/
|
|
59
63
|
registerHeaderItem(name, component, options = {}) {
|
|
64
|
+
if (options.home === true)
|
|
65
|
+
this.#homeItems.add(name);
|
|
66
|
+
else
|
|
67
|
+
this.#homeItems.delete(name);
|
|
60
68
|
return this.#add('header', name, component, options.replace === true);
|
|
61
69
|
}
|
|
62
70
|
#userPageLabels = new Map();
|
|
@@ -83,6 +91,7 @@ export class Registry {
|
|
|
83
91
|
return [...this.#entries.header].map(([name, component]) => ({
|
|
84
92
|
name,
|
|
85
93
|
component: component,
|
|
94
|
+
home: this.#homeItems.has(name),
|
|
86
95
|
}));
|
|
87
96
|
}
|
|
88
97
|
/** A control next to every search box (the AI assistant's "Ask"). */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved views as tabs (RFC 0018 §4.3): above the search, *All* and the saved searches of this model and view type —
|
|
3
|
+
* the user's own and the shared ones —, the one matching the current search marked. A tab applies its search; more
|
|
4
|
+
* than a handful go into a *More* menu. Saving and the full list stay in the search bar's favorites menu; with
|
|
5
|
+
* nothing saved, there are no tabs.
|
|
6
|
+
*/
|
|
7
|
+
import { type SearchState } from '../../search/index.js';
|
|
8
|
+
import type { ReactElement } from 'react';
|
|
9
|
+
export declare function ViewTabs({ model, viewType, state, onChange, }: {
|
|
10
|
+
readonly model: string;
|
|
11
|
+
readonly viewType: string;
|
|
12
|
+
readonly state: SearchState;
|
|
13
|
+
readonly onChange: (state: SearchState) => void;
|
|
14
|
+
}): ReactElement | null;
|