@things-factory/shell 10.1.34 → 10.1.38
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-server/service/subscription-data/data-resolver.js +2 -3
- package/dist-server/service/subscription-data/data-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/index.d.ts +1 -0
- package/dist-server/utils/index.js +1 -0
- package/dist-server/utils/index.js.map +1 -1
- package/dist-server/utils/subscribe-auth.d.ts +14 -0
- package/dist-server/utils/subscribe-auth.js +35 -0
- package/dist-server/utils/subscribe-auth.js.map +1 -0
- package/package.json +2 -2
- package/tests/popup-buttons-class-shrinks.test.ts +15 -12
- package/tests/subscribe-auth.test.ts +57 -0
- package/tests/theme-file-names.test.ts +6 -10
- package/translations/en.json +4 -1
- package/translations/ja.json +4 -1
- package/translations/ko.json +4 -1
- package/translations/ms.json +4 -1
- package/translations/zh.json +3 -1
|
@@ -12,4 +12,5 @@ tslib_1.__exportStar(require("./keyset-cursor.js"), exports);
|
|
|
12
12
|
tslib_1.__exportStar(require("./list-param-adjuster.js"), exports);
|
|
13
13
|
tslib_1.__exportStar(require("./get-times-for-period.js"), exports);
|
|
14
14
|
tslib_1.__exportStar(require("./headless-pool/index.js"), exports);
|
|
15
|
+
tslib_1.__exportStar(require("./subscribe-auth.js"), exports);
|
|
15
16
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/utils/index.ts"],"names":[],"mappings":";;;AAAA,0DAA+B;AAC/B,mEAAwC;AACxC,iEAAsC;AACtC,kEAAuC;AACvC,qEAA0C;AAC1C,gEAAqC;AACrC,kFAAuD;AACvD,6DAAkC;AAClC,mEAAwC;AACxC,oEAAyC;AACzC,mEAAwC","sourcesContent":["export * from './get-domain.js'\nexport * from './apply-domain-filter.js'\nexport * from './condition-builder.js'\nexport * from './list-query-builder.js'\nexport * from './list-params-converter.js'\nexport * from './publish-progress.js'\nexport * from './get-query-builder-from-list-params.js'\nexport * from './keyset-cursor.js'\nexport * from './list-param-adjuster.js'\nexport * from './get-times-for-period.js'\nexport * from './headless-pool/index.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/utils/index.ts"],"names":[],"mappings":";;;AAAA,0DAA+B;AAC/B,mEAAwC;AACxC,iEAAsC;AACtC,kEAAuC;AACvC,qEAA0C;AAC1C,gEAAqC;AACrC,kFAAuD;AACvD,6DAAkC;AAClC,mEAAwC;AACxC,oEAAyC;AACzC,mEAAwC;AACxC,8DAAmC","sourcesContent":["export * from './get-domain.js'\nexport * from './apply-domain-filter.js'\nexport * from './condition-builder.js'\nexport * from './list-query-builder.js'\nexport * from './list-params-converter.js'\nexport * from './publish-progress.js'\nexport * from './get-query-builder-from-list-params.js'\nexport * from './keyset-cursor.js'\nexport * from './list-param-adjuster.js'\nexport * from './get-times-for-period.js'\nexport * from './headless-pool/index.js'\nexport * from './subscribe-auth.js'\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 구독 허용 여부 단언. 불가 시 throw (graphql-ws 가 구독 거부).
|
|
3
|
+
* - domain 없음 → 거부.
|
|
4
|
+
* - user 가 해당 subdomain 의 도메인 소속이 아니고 superUser 도 아니면 → 거부.
|
|
5
|
+
*
|
|
6
|
+
* 주의(버그 수정): process.superUserGranted 는 async(Promise<boolean>) 다. 기존 양쪽 spine 은
|
|
7
|
+
* `!process.superUserGranted(...)` 로 동기 평가해 `!Promise` = 항상 false → membership throw 가
|
|
8
|
+
* 발동하지 않는 fail-open 상태였다. 여기서는 await 로 제대로 평가한다 → subscribe 는 async.
|
|
9
|
+
*
|
|
10
|
+
* @param domain context.state.domain
|
|
11
|
+
* @param user context.state.user (domains[] / email 사용)
|
|
12
|
+
* @param isSuperUser (domain, user) → boolean | Promise<boolean> (process.superUserGranted 주입)
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertDomainSubscribeAllowed(domain: any, user: any, isSuperUser: (domain: any, user: any) => boolean | Promise<boolean>): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 구독 도메인 인증 게이트 — 순수 로직 (데코레이터·pubsub 없음 → 테스트 가능).
|
|
4
|
+
*
|
|
5
|
+
* 실시간 구독(@Subscription)은 같은 도메인 구성원만 허용해야 한다(도메인 격리 = 보안 핵심).
|
|
6
|
+
* 셸의 `data` 구독 · ai-assistant 의 chat-message · session-activity · board-ai 의 board-patch ·
|
|
7
|
+
* headless-twin 의 구독이 이 하나를 부른다(reuse — fork 금지). 가장 낮은 층에 둔다 — 부르는 쪽이 모두
|
|
8
|
+
* 이미 셸 위에 선다(2026-09-24 ai-assistant 에서 옮김). superUserGranted 는 process 전역 의존이라
|
|
9
|
+
* 주입받아 순수성 유지.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.assertDomainSubscribeAllowed = assertDomainSubscribeAllowed;
|
|
13
|
+
/**
|
|
14
|
+
* 구독 허용 여부 단언. 불가 시 throw (graphql-ws 가 구독 거부).
|
|
15
|
+
* - domain 없음 → 거부.
|
|
16
|
+
* - user 가 해당 subdomain 의 도메인 소속이 아니고 superUser 도 아니면 → 거부.
|
|
17
|
+
*
|
|
18
|
+
* 주의(버그 수정): process.superUserGranted 는 async(Promise<boolean>) 다. 기존 양쪽 spine 은
|
|
19
|
+
* `!process.superUserGranted(...)` 로 동기 평가해 `!Promise` = 항상 false → membership throw 가
|
|
20
|
+
* 발동하지 않는 fail-open 상태였다. 여기서는 await 로 제대로 평가한다 → subscribe 는 async.
|
|
21
|
+
*
|
|
22
|
+
* @param domain context.state.domain
|
|
23
|
+
* @param user context.state.user (domains[] / email 사용)
|
|
24
|
+
* @param isSuperUser (domain, user) → boolean | Promise<boolean> (process.superUserGranted 주입)
|
|
25
|
+
*/
|
|
26
|
+
async function assertDomainSubscribeAllowed(domain, user, isSuperUser) {
|
|
27
|
+
if (!domain) {
|
|
28
|
+
throw new Error('domain required');
|
|
29
|
+
}
|
|
30
|
+
const subdomain = domain?.subdomain;
|
|
31
|
+
if (!user?.domains?.find((d) => d.subdomain === subdomain) && !(await isSuperUser(domain, user))) {
|
|
32
|
+
throw new Error(`domain(${subdomain}) is not working for user(${user?.email}).`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=subscribe-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe-auth.js","sourceRoot":"","sources":["../../server/utils/subscribe-auth.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAeH,oEAYC;AAzBD;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,4BAA4B,CAChD,MAAW,EACX,IAAS,EACT,WAAmE;IAEnE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;IACpC,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,EAAE,SAAS,CAAA;IACnC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;QACtG,MAAM,IAAI,KAAK,CAAC,UAAU,SAAS,6BAA6B,IAAI,EAAE,KAAK,IAAI,CAAC,CAAA;IAClF,CAAC;AACH,CAAC","sourcesContent":["/*\n * 구독 도메인 인증 게이트 — 순수 로직 (데코레이터·pubsub 없음 → 테스트 가능).\n *\n * 실시간 구독(@Subscription)은 같은 도메인 구성원만 허용해야 한다(도메인 격리 = 보안 핵심).\n * 셸의 `data` 구독 · ai-assistant 의 chat-message · session-activity · board-ai 의 board-patch ·\n * headless-twin 의 구독이 이 하나를 부른다(reuse — fork 금지). 가장 낮은 층에 둔다 — 부르는 쪽이 모두\n * 이미 셸 위에 선다(2026-09-24 ai-assistant 에서 옮김). superUserGranted 는 process 전역 의존이라\n * 주입받아 순수성 유지.\n */\n\n/**\n * 구독 허용 여부 단언. 불가 시 throw (graphql-ws 가 구독 거부).\n * - domain 없음 → 거부.\n * - user 가 해당 subdomain 의 도메인 소속이 아니고 superUser 도 아니면 → 거부.\n *\n * 주의(버그 수정): process.superUserGranted 는 async(Promise<boolean>) 다. 기존 양쪽 spine 은\n * `!process.superUserGranted(...)` 로 동기 평가해 `!Promise` = 항상 false → membership throw 가\n * 발동하지 않는 fail-open 상태였다. 여기서는 await 로 제대로 평가한다 → subscribe 는 async.\n *\n * @param domain context.state.domain\n * @param user context.state.user (domains[] / email 사용)\n * @param isSuperUser (domain, user) → boolean | Promise<boolean> (process.superUserGranted 주입)\n */\nexport async function assertDomainSubscribeAllowed(\n domain: any,\n user: any,\n isSuperUser: (domain: any, user: any) => boolean | Promise<boolean>\n): Promise<void> {\n if (!domain) {\n throw new Error('domain required')\n }\n const subdomain = domain?.subdomain\n if (!user?.domains?.find((d: any) => d.subdomain === subdomain) && !(await isSuperUser(domain, user))) {\n throw new Error(`domain(${subdomain}) is not working for user(${user?.email}).`)\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/shell",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.38",
|
|
4
4
|
"description": "Core module for framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"things-factory": "bin/things-factory",
|
|
@@ -126,5 +126,5 @@
|
|
|
126
126
|
"pg": "^8.7.3",
|
|
127
127
|
"sqlite3": "^5.0.8"
|
|
128
128
|
},
|
|
129
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "b947349fe1c27d11038b592ceadaba336f14785b"
|
|
130
130
|
}
|
|
@@ -27,17 +27,15 @@ const PACKAGES = path.join(REPO_ROOT, 'packages')
|
|
|
27
27
|
|
|
28
28
|
const SKIP = new Set(['node_modules', 'dist', 'dist-client', 'dist-server', 'dist-shared', 'coverage'])
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'organization/client/component/recipients-editor-popup.ts'
|
|
40
|
-
]
|
|
30
|
+
/**
|
|
31
|
+
* Files that still render the CommonPopupStyles `.buttons` row.
|
|
32
|
+
*
|
|
33
|
+
* Empty since 2026-09-24: auth-ui's three editors and organization's five popups moved to
|
|
34
|
+
* `<ox-popup-actions>`. The list stays so that a new hand-drawn row is still caught. The `.buttons`
|
|
35
|
+
* rule itself stays in `@operato/styles` - popups outside this repository (dssp, fmsim) still draw
|
|
36
|
+
* with it, and removing it is a decision to take with them.
|
|
37
|
+
*/
|
|
38
|
+
const STILL_USING: string[] = []
|
|
41
39
|
|
|
42
40
|
function clientSources(dir: string, found: string[] = []): string[] {
|
|
43
41
|
for (const entry of readdirSync(dir)) {
|
|
@@ -52,7 +50,12 @@ function clientSources(dir: string, found: string[] = []): string[] {
|
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
if (stat.isDirectory()) clientSources(full, found)
|
|
55
|
-
|
|
53
|
+
/*
|
|
54
|
+
* Tests are skipped: they never render a popup, and a test that guards the row has to name the
|
|
55
|
+
* class it forbids - which this scan would then count as a use (it did, 2026-09-24).
|
|
56
|
+
*/
|
|
57
|
+
else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts') && full.includes(`${path.sep}client${path.sep}`))
|
|
58
|
+
found.push(full)
|
|
56
59
|
}
|
|
57
60
|
return found
|
|
58
61
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 구독 도메인 인증 게이트 — 단위 테스트 (보안 핵심: 도메인 격리).
|
|
3
|
+
* board-patch / chat-message 양쪽 spine 이 공유하는 게이트.
|
|
4
|
+
*
|
|
5
|
+
* 회귀 방지: process.superUserGranted 는 async 다. 동기 `!process.superUserGranted(...)` 로
|
|
6
|
+
* 평가하면 `!Promise` = 항상 false → membership throw 가 죽는다(fail-open). 게이트는 반드시
|
|
7
|
+
* await 로 평가해야 한다. 아래 superUser 분기 테스트가 그 회귀를 박는다.
|
|
8
|
+
*/
|
|
9
|
+
import { assertDomainSubscribeAllowed } from '../server/utils/subscribe-auth'
|
|
10
|
+
|
|
11
|
+
const never = async () => false // superUser 아님 (async — 실제 process.superUserGranted 와 동일)
|
|
12
|
+
const always = async () => true // superUser (async)
|
|
13
|
+
|
|
14
|
+
const domain = { id: 'd1', subdomain: 'acme' }
|
|
15
|
+
const userInDomain = { email: 'a@x.io', domains: [{ subdomain: 'acme' }, { subdomain: 'other' }] }
|
|
16
|
+
const userOutDomain = { email: 'b@x.io', domains: [{ subdomain: 'other' }] }
|
|
17
|
+
|
|
18
|
+
describe('assertDomainSubscribeAllowed (도메인 격리 게이트)', () => {
|
|
19
|
+
it('domain 없으면 → reject (domain required)', async () => {
|
|
20
|
+
await expect(assertDomainSubscribeAllowed(undefined, userInDomain, never)).rejects.toThrow('domain required')
|
|
21
|
+
await expect(assertDomainSubscribeAllowed(null, userInDomain, never)).rejects.toThrow('domain required')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('user 가 해당 subdomain 소속 → 통과 (superUser 검사 불필요)', async () => {
|
|
25
|
+
await expect(assertDomainSubscribeAllowed(domain, userInDomain, never)).resolves.toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('user 가 미소속 + superUser 아님 → reject (격리 — 핵심)', async () => {
|
|
29
|
+
await expect(assertDomainSubscribeAllowed(domain, userOutDomain, never)).rejects.toThrow(
|
|
30
|
+
'domain(acme) is not working for user(b@x.io).'
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('user 가 미소속이어도 superUser(async resolve true) 면 → 통과 (await 회귀 방지)', async () => {
|
|
35
|
+
await expect(assertDomainSubscribeAllowed(domain, userOutDomain, always)).resolves.toBeUndefined()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('user.domains 없음 + superUser 아님 → reject', async () => {
|
|
39
|
+
await expect(assertDomainSubscribeAllowed(domain, { email: 'c@x.io' }, never)).rejects.toThrow(
|
|
40
|
+
'domain(acme) is not working for user(c@x.io).'
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('user.domains 없음 + superUser → 통과', async () => {
|
|
45
|
+
await expect(assertDomainSubscribeAllowed(domain, { email: 'c@x.io' }, always)).resolves.toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('isSuperUser 는 domain+user 를 받아 호출되고 await 된다', async () => {
|
|
49
|
+
const calls: any[] = []
|
|
50
|
+
const spy = async (d: any, u: any) => {
|
|
51
|
+
calls.push([d, u])
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
await assertDomainSubscribeAllowed(domain, userOutDomain, spy)
|
|
55
|
+
expect(calls).toEqual([[domain, userOutDomain]])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -25,18 +25,14 @@ const PACKAGES = path.resolve(__dirname, '..', '..')
|
|
|
25
25
|
const SHELL_THEMES = path.resolve(__dirname, '..', 'client', 'themes')
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Files that carry a shell theme file's name today.
|
|
28
|
+
* Files that carry a shell theme file's name today - none, since 2026-09-24.
|
|
29
29
|
*
|
|
30
|
-
* -
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* figure-ui moved its palette out of themes/ (e638595af8). The generator templates, which gave every
|
|
31
|
+
* new application a light.css and dark.css - operato-warehouse was born replacing the shell theme
|
|
32
|
+
* that way - now give it `{{name}}-accent.css`, after the four operato products had moved. The list
|
|
33
|
+
* stays empty so a new one is caught.
|
|
33
34
|
*/
|
|
34
|
-
const KNOWN: string[] = [
|
|
35
|
-
'generator/templates/app/client/themes/dark.css',
|
|
36
|
-
'generator/templates/app/client/themes/light.css',
|
|
37
|
-
'generator/templates/brand/client/themes/dark.css',
|
|
38
|
-
'generator/templates/brand/client/themes/light.css'
|
|
39
|
-
]
|
|
35
|
+
const KNOWN: string[] = []
|
|
40
36
|
|
|
41
37
|
/** Every `client/themes/*.css` under packages, as a path relative to packages. */
|
|
42
38
|
function themeFiles(): string[] {
|
package/translations/en.json
CHANGED
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"text.there_is_nothing_to_process": "there is nothing to process",
|
|
148
148
|
"text.there_is_nothing_to_save": "there is nothing to save",
|
|
149
149
|
"text.you.are.now.in": "you are now in {state}",
|
|
150
|
+
"title.approval-line editor": "approval line editor",
|
|
150
151
|
"title.activity list": "activity list",
|
|
151
152
|
"title.activity model item list": "activity model item list",
|
|
152
153
|
"title.approval-line item list": "approval line item list",
|
|
@@ -155,5 +156,7 @@
|
|
|
155
156
|
"title.event list": "event list",
|
|
156
157
|
"title.import event": "import events",
|
|
157
158
|
"title.lookup role": "lookup role",
|
|
158
|
-
"title.select_item": "select item"
|
|
159
|
+
"title.select_item": "select item",
|
|
160
|
+
"term.domain": "site",
|
|
161
|
+
"field.roles": "roles"
|
|
159
162
|
}
|
package/translations/ja.json
CHANGED
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"text.there_is_nothing_to_process": "処理するデータがありません.",
|
|
148
148
|
"text.there_is_nothing_to_save": "保存するデータがありません.",
|
|
149
149
|
"text.you.are.now.in": "{state}になりました.",
|
|
150
|
+
"title.approval-line editor": "決裁ライン編集",
|
|
150
151
|
"title.activity list": "業務定義リスト",
|
|
151
152
|
"title.activity model item list": "業務モデルリスト",
|
|
152
153
|
"title.approval-line item list": "決裁ライン詳細",
|
|
@@ -155,5 +156,7 @@
|
|
|
155
156
|
"title.event list": "イベント一覧",
|
|
156
157
|
"title.import event": "イベントの取り込み",
|
|
157
158
|
"title.lookup role": "ロール検索",
|
|
158
|
-
"title.select_item": "アイテム選択"
|
|
159
|
+
"title.select_item": "アイテム選択",
|
|
160
|
+
"term.domain": "事業場",
|
|
161
|
+
"field.roles": "ロール"
|
|
159
162
|
}
|
package/translations/ko.json
CHANGED
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"text.there_is_nothing_to_process": "처리할 데이터가 없습니다.",
|
|
148
148
|
"text.there_is_nothing_to_save": "저장할 데이터가 없습니다.",
|
|
149
149
|
"text.you.are.now.in": "{state} 되었습니다.",
|
|
150
|
+
"title.approval-line editor": "결재선 편집",
|
|
150
151
|
"title.activity list": "업무정의 리스트",
|
|
151
152
|
"title.activity model item list": "업무 모델 항목",
|
|
152
153
|
"title.approval-line item list": "결재선 상세",
|
|
@@ -155,5 +156,7 @@
|
|
|
155
156
|
"title.event list": "일정",
|
|
156
157
|
"title.import event": "일정 가져오기",
|
|
157
158
|
"title.lookup role": "역할 검색",
|
|
158
|
-
"title.select_item": "아이템 선택"
|
|
159
|
+
"title.select_item": "아이템 선택",
|
|
160
|
+
"term.domain": "사업장",
|
|
161
|
+
"field.roles": "역할"
|
|
159
162
|
}
|
package/translations/ms.json
CHANGED
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"text.there_is_nothing_to_process": "there is nothing to process",
|
|
148
148
|
"text.there_is_nothing_to_save": "there is nothing to save",
|
|
149
149
|
"text.you.are.now.in": "anda berada di {state}",
|
|
150
|
+
"title.approval-line editor": "editor garis kelulusan",
|
|
150
151
|
"title.activity list": "senarai aktiviti",
|
|
151
152
|
"title.activity model item list": "senarai item model aktiviti",
|
|
152
153
|
"title.approval-line item list": "senarai item garis kelulusan",
|
|
@@ -155,5 +156,7 @@
|
|
|
155
156
|
"title.event list": "senarai acara",
|
|
156
157
|
"title.import event": "import acara",
|
|
157
158
|
"title.lookup role": "cari peranan",
|
|
158
|
-
"title.select_item": "pilih item"
|
|
159
|
+
"title.select_item": "pilih item",
|
|
160
|
+
"term.domain": "tapak",
|
|
161
|
+
"field.roles": "peranan"
|
|
159
162
|
}
|
package/translations/zh.json
CHANGED
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"text.there_is_nothing_to_process": "没有要处理的数据。",
|
|
148
148
|
"text.there_is_nothing_to_save": "没有要保存的数据。",
|
|
149
149
|
"text.you.are.now.in": "您现在是 {state}。",
|
|
150
|
+
"title.approval-line editor": "审批线编辑器",
|
|
150
151
|
"title.activity list": "业务列表",
|
|
151
152
|
"title.activity model item list": "业务模型项目列表",
|
|
152
153
|
"title.approval-line item list": "审批线项目列表",
|
|
@@ -155,5 +156,6 @@
|
|
|
155
156
|
"title.event list": "事件列表",
|
|
156
157
|
"title.import event": "导入事件",
|
|
157
158
|
"title.lookup role": "查找角色",
|
|
158
|
-
"title.select_item": "选择项目"
|
|
159
|
+
"title.select_item": "选择项目",
|
|
160
|
+
"field.roles": "角色"
|
|
159
161
|
}
|