@cratis/scene.blueprint.default 4.35.0 → 4.36.0
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/ComponentName.ts +12 -0
- package/defaultBlueprintComponents.ts +5 -1
- package/dist/cjs/ComponentName.js +4 -0
- package/dist/cjs/ComponentName.js.map +1 -1
- package/dist/cjs/defaultBlueprint.js +1 -0
- package/dist/cjs/defaultBlueprint.js.map +1 -1
- package/dist/cjs/defaultBlueprintComponentMetadata.js +1 -0
- package/dist/cjs/defaultBlueprintComponentMetadata.js.map +1 -1
- package/dist/cjs/defaultBlueprintComponents.js +9 -1
- package/dist/cjs/defaultBlueprintComponents.js.map +1 -1
- package/dist/cjs/foundation/ActivityFeed.js +122 -0
- package/dist/cjs/foundation/ActivityFeed.js.map +1 -0
- package/dist/cjs/foundation/AvatarStack.js +73 -0
- package/dist/cjs/foundation/AvatarStack.js.map +1 -0
- package/dist/cjs/foundation/ChecklistSection.js +174 -0
- package/dist/cjs/foundation/ChecklistSection.js.map +1 -0
- package/dist/cjs/foundation/FileUploader.js +218 -0
- package/dist/cjs/foundation/FileUploader.js.map +1 -0
- package/dist/cjs/foundation/FileUploaderHandler.js +17 -0
- package/dist/cjs/foundation/FileUploaderHandler.js.map +1 -0
- package/dist/cjs/foundation/activityModel.js +63 -0
- package/dist/cjs/foundation/activityModel.js.map +1 -0
- package/dist/cjs/foundation/avatarModel.js +42 -0
- package/dist/cjs/foundation/avatarModel.js.map +1 -0
- package/dist/cjs/foundation/checklistModel.js +59 -0
- package/dist/cjs/foundation/checklistModel.js.map +1 -0
- package/dist/cjs/foundation/formatting.js +11 -0
- package/dist/cjs/foundation/formatting.js.map +1 -1
- package/dist/cjs/foundation/foundationComponentMetadata.js +26 -0
- package/dist/cjs/foundation/foundationComponentMetadata.js.map +1 -1
- package/dist/cjs/foundation/foundationDescriptors.js +128 -0
- package/dist/cjs/foundation/foundationDescriptors.js.map +1 -1
- package/dist/cjs/foundation/foundationSamples.js +114 -0
- package/dist/cjs/foundation/foundationSamples.js.map +1 -1
- package/dist/cjs/foundation/foundationStrings.js +15 -0
- package/dist/cjs/foundation/foundationStrings.js.map +1 -1
- package/dist/cjs/foundation/uploadModel.js +71 -0
- package/dist/cjs/foundation/uploadModel.js.map +1 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/ComponentName.d.ts +5 -1
- package/dist/esm/ComponentName.d.ts.map +1 -1
- package/dist/esm/ComponentName.js +4 -0
- package/dist/esm/ComponentName.js.map +1 -1
- package/dist/esm/defaultBlueprint.js +1 -0
- package/dist/esm/defaultBlueprint.js.map +1 -1
- package/dist/esm/defaultBlueprintComponentMetadata.js +1 -0
- package/dist/esm/defaultBlueprintComponentMetadata.js.map +1 -1
- package/dist/esm/defaultBlueprintComponents.d.ts.map +1 -1
- package/dist/esm/defaultBlueprintComponents.js +9 -1
- package/dist/esm/defaultBlueprintComponents.js.map +1 -1
- package/dist/esm/foundation/ActivityFeed.d.ts +3 -0
- package/dist/esm/foundation/ActivityFeed.d.ts.map +1 -0
- package/dist/esm/foundation/ActivityFeed.js +120 -0
- package/dist/esm/foundation/ActivityFeed.js.map +1 -0
- package/dist/esm/foundation/AvatarStack.d.ts +3 -0
- package/dist/esm/foundation/AvatarStack.d.ts.map +1 -0
- package/dist/esm/foundation/AvatarStack.js +71 -0
- package/dist/esm/foundation/AvatarStack.js.map +1 -0
- package/dist/esm/foundation/ChecklistSection.d.ts +3 -0
- package/dist/esm/foundation/ChecklistSection.d.ts.map +1 -0
- package/dist/esm/foundation/ChecklistSection.js +172 -0
- package/dist/esm/foundation/ChecklistSection.js.map +1 -0
- package/dist/esm/foundation/FileUploader.d.ts +3 -0
- package/dist/esm/foundation/FileUploader.d.ts.map +1 -0
- package/dist/esm/foundation/FileUploader.js +216 -0
- package/dist/esm/foundation/FileUploader.js.map +1 -0
- package/dist/esm/foundation/FileUploaderHandler.d.ts +8 -0
- package/dist/esm/foundation/FileUploaderHandler.d.ts.map +1 -0
- package/dist/esm/foundation/FileUploaderHandler.js +14 -0
- package/dist/esm/foundation/FileUploaderHandler.js.map +1 -0
- package/dist/esm/foundation/Foundation.stories.d.ts +8 -0
- package/dist/esm/foundation/Foundation.stories.d.ts.map +1 -1
- package/dist/esm/foundation/Foundation.stories.js +8 -0
- package/dist/esm/foundation/Foundation.stories.js.map +1 -1
- package/dist/esm/foundation/activityModel.d.ts +22 -0
- package/dist/esm/foundation/activityModel.d.ts.map +1 -0
- package/dist/esm/foundation/activityModel.js +58 -0
- package/dist/esm/foundation/activityModel.js.map +1 -0
- package/dist/esm/foundation/avatarModel.d.ts +11 -0
- package/dist/esm/foundation/avatarModel.d.ts.map +1 -0
- package/dist/esm/foundation/avatarModel.js +38 -0
- package/dist/esm/foundation/avatarModel.js.map +1 -0
- package/dist/esm/foundation/checklistModel.d.ts +17 -0
- package/dist/esm/foundation/checklistModel.d.ts.map +1 -0
- package/dist/esm/foundation/checklistModel.js +53 -0
- package/dist/esm/foundation/checklistModel.js.map +1 -0
- package/dist/esm/foundation/formatting.d.ts +1 -0
- package/dist/esm/foundation/formatting.d.ts.map +1 -1
- package/dist/esm/foundation/formatting.js +11 -1
- package/dist/esm/foundation/formatting.js.map +1 -1
- package/dist/esm/foundation/foundationComponentMetadata.d.ts.map +1 -1
- package/dist/esm/foundation/foundationComponentMetadata.js +26 -0
- package/dist/esm/foundation/foundationComponentMetadata.js.map +1 -1
- package/dist/esm/foundation/foundationDescriptors.d.ts.map +1 -1
- package/dist/esm/foundation/foundationDescriptors.js +128 -0
- package/dist/esm/foundation/foundationDescriptors.js.map +1 -1
- package/dist/esm/foundation/foundationSamples.d.ts.map +1 -1
- package/dist/esm/foundation/foundationSamples.js +114 -0
- package/dist/esm/foundation/foundationSamples.js.map +1 -1
- package/dist/esm/foundation/foundationStrings.d.ts +15 -0
- package/dist/esm/foundation/foundationStrings.d.ts.map +1 -1
- package/dist/esm/foundation/foundationStrings.js +15 -0
- package/dist/esm/foundation/foundationStrings.js.map +1 -1
- package/dist/esm/foundation/index.d.ts +9 -0
- package/dist/esm/foundation/index.d.ts.map +1 -1
- package/dist/esm/foundation/index.js +9 -0
- package/dist/esm/foundation/index.js.map +1 -1
- package/dist/esm/foundation/uploadModel.d.ts +31 -0
- package/dist/esm/foundation/uploadModel.d.ts.map +1 -0
- package/dist/esm/foundation/uploadModel.js +65 -0
- package/dist/esm/foundation/uploadModel.js.map +1 -0
- package/dist/esm/index.js +10 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/foundation/ActivityFeed.tsx +61 -0
- package/foundation/AvatarStack.tsx +43 -0
- package/foundation/ChecklistSection.tsx +80 -0
- package/foundation/FileUploader.tsx +122 -0
- package/foundation/FileUploaderHandler.tsx +22 -0
- package/foundation/Foundation.stories.tsx +8 -0
- package/foundation/activityModel.ts +65 -0
- package/foundation/avatarModel.ts +36 -0
- package/foundation/checklistModel.ts +62 -0
- package/foundation/formatting.ts +9 -0
- package/foundation/foundationComponentMetadata.ts +4 -0
- package/foundation/foundationDescriptors.ts +79 -0
- package/foundation/foundationSamples.ts +29 -0
- package/foundation/foundationStrings.ts +15 -0
- package/foundation/index.ts +9 -0
- package/foundation/uploadModel.ts +66 -0
- package/package.json +8 -8
- package/shell/layout.css +234 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { RegisteredComponentProps } from '@cratis/scene.react';
|
|
6
|
+
import { readOptionalString } from '../shell/elementProperties';
|
|
7
|
+
import { ActivityKind, groupByDay, readActivity } from './activityModel';
|
|
8
|
+
import { ComponentState, readComponentState } from './ComponentState';
|
|
9
|
+
import { FoundationIcon, FoundationIconName } from './FoundationIcon';
|
|
10
|
+
import { formatDate, formatDay, readLocale } from './formatting';
|
|
11
|
+
import { readFoundationStrings } from './foundationStrings';
|
|
12
|
+
import { FoundationFrame } from './StateFrame';
|
|
13
|
+
|
|
14
|
+
const icons: Record<ActivityKind, FoundationIconName> = {
|
|
15
|
+
[ActivityKind.Comment]: 'user', [ActivityKind.Change]: 'refresh', [ActivityKind.Status]: 'check', [ActivityKind.System]: 'info',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The `activityFeed` component: what happened to a record, newest first, under a heading for each day.
|
|
20
|
+
*
|
|
21
|
+
* `entries` carry an id, who (`actor`), what (`action`), the instant (`at`, ISO 8601), a `kind` and an optional `body` such as a comment's
|
|
22
|
+
* text. The feed is an ordered list; each entry names its kind in words for assistive technology as well as by icon, and its time is a
|
|
23
|
+
* `time` element formatted for the locale. Content is supplied by the application - this only lays it out - and a feed of any length
|
|
24
|
+
* stays one list. `order="ascending"` reads oldest first, and `timeZone` fixes the zone days are cut in.
|
|
25
|
+
*/
|
|
26
|
+
export function ActivityFeed({ element, interactions }: RegisteredComponentProps) {
|
|
27
|
+
const entries = useMemo(() => readActivity(element.properties.entries), [element.properties.entries]);
|
|
28
|
+
const strings = readFoundationStrings(element);
|
|
29
|
+
const locale = readLocale(element);
|
|
30
|
+
const timeZone = readOptionalString(element, 'timeZone');
|
|
31
|
+
const days = useMemo(() => groupByDay(entries, element.properties.order === 'ascending', timeZone), [entries, element.properties.order, timeZone]);
|
|
32
|
+
const state = readComponentState(element, entries.length > 0);
|
|
33
|
+
const kindText: Record<ActivityKind, string> = { comment: strings.activityComment, change: strings.activityChange, status: strings.activityStatus, system: strings.activitySystem };
|
|
34
|
+
const emptyElement = state === ComponentState.Empty ? { ...element, properties: { emptyHeadline: strings.activityEmpty, ...element.properties } } : element;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<FoundationFrame element={emptyElement} interactions={interactions} name='activity-feed' state={state} skeleton={[60, 90, 70, 85]}>
|
|
38
|
+
{(state === ComponentState.Ready || state === ComponentState.Partial) && days.map(day => (
|
|
39
|
+
<section key={day.day} className='foundation-activity-day'>
|
|
40
|
+
<h3 className='foundation-activity-heading'><time dateTime={day.day}>{formatDay(day.day, locale)}</time></h3>
|
|
41
|
+
<ol className='foundation-activity-list'>
|
|
42
|
+
{day.entries.map(entry => (
|
|
43
|
+
<li key={entry.id} className='foundation-activity-entry' data-kind={entry.kind}>
|
|
44
|
+
<span className='foundation-activity-icon'><FoundationIcon name={icons[entry.kind]} /></span>
|
|
45
|
+
<div className='foundation-activity-content'>
|
|
46
|
+
<p className='foundation-activity-summary'>
|
|
47
|
+
<span className='foundation-visually-hidden'>{`${kindText[entry.kind]}: `}</span>
|
|
48
|
+
<strong>{entry.actor}</strong> {entry.action}
|
|
49
|
+
</p>
|
|
50
|
+
{entry.body && <p className='foundation-activity-body'>{entry.body}</p>}
|
|
51
|
+
<time className='foundation-count' dateTime={entry.at}>{formatDate(entry.at, locale, true)}</time>
|
|
52
|
+
</div>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ol>
|
|
56
|
+
</section>
|
|
57
|
+
))}
|
|
58
|
+
</FoundationFrame>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { RegisteredComponentProps } from '@cratis/scene.react';
|
|
6
|
+
import { initialsOf, readPeople, visiblePeople } from './avatarModel';
|
|
7
|
+
import { ComponentState, readComponentState } from './ComponentState';
|
|
8
|
+
import { formatText, readFoundationStrings } from './foundationStrings';
|
|
9
|
+
import { FoundationFrame } from './StateFrame';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `avatarStack` component: overlapping avatars for the people on a record, with a "+N" chip for the rest.
|
|
13
|
+
*
|
|
14
|
+
* `people` are names with an optional picture; without one the avatar shows the person's initials. The stack is a labelled group ("5 people"),
|
|
15
|
+
* each avatar is an image named after its person, and the overflow chip says how many more in words. `max` is how many are shown (4 by
|
|
16
|
+
* default) and `size` is `small` or `medium`.
|
|
17
|
+
*/
|
|
18
|
+
export function AvatarStack({ element, interactions }: RegisteredComponentProps) {
|
|
19
|
+
const people = useMemo(() => readPeople(element.properties.people), [element.properties.people]);
|
|
20
|
+
const strings = readFoundationStrings(element);
|
|
21
|
+
const { shown, hidden } = visiblePeople(people, typeof element.properties.max === 'number' ? element.properties.max : 4);
|
|
22
|
+
const state = readComponentState(element, people.length > 0);
|
|
23
|
+
const size = element.properties.size === 'small' ? 'small' : 'medium';
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<FoundationFrame element={element} interactions={interactions} name='avatar-stack' state={state} skeleton={[35]}>
|
|
27
|
+
{(state === ComponentState.Ready || state === ComponentState.Partial) && (
|
|
28
|
+
<ul className='foundation-avatars' role='group' aria-label={formatText(strings.avatarPeople, { count: people.length })} data-size={size}>
|
|
29
|
+
{shown.map(person => (
|
|
30
|
+
<li key={person.name} className='foundation-avatar' role='img' aria-label={person.name} title={person.name}>
|
|
31
|
+
{person.imageUrl ? <img src={person.imageUrl} alt='' /> : <span aria-hidden='true'>{initialsOf(person.name)}</span>}
|
|
32
|
+
</li>
|
|
33
|
+
))}
|
|
34
|
+
{hidden > 0 && (
|
|
35
|
+
<li className='foundation-avatar foundation-avatar-more' role='img' aria-label={formatText(strings.avatarOverflow, { count: hidden })}>
|
|
36
|
+
<span aria-hidden='true'>+{hidden}</span>
|
|
37
|
+
</li>
|
|
38
|
+
)}
|
|
39
|
+
</ul>
|
|
40
|
+
)}
|
|
41
|
+
</FoundationFrame>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { RegisteredComponentProps, useValueOutput } from '@cratis/scene.react';
|
|
6
|
+
import { ChecklistGroup, completed, doneTaskIds, isOverdue, readChecklist, toggleTask } from './checklistModel';
|
|
7
|
+
import { ComponentState, readComponentState } from './ComponentState';
|
|
8
|
+
import { FoundationIcon } from './FoundationIcon';
|
|
9
|
+
import { formatDate, readLocale } from './formatting';
|
|
10
|
+
import { formatText, readFoundationStrings } from './foundationStrings';
|
|
11
|
+
import { readPermissionRule } from './PermissionRule';
|
|
12
|
+
import { FoundationFrame } from './StateFrame';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The `checklistSection` component: sections of tasks, each with its owner and due date, and a progress bar per section.
|
|
16
|
+
*
|
|
17
|
+
* `sections` give the structure (`title`, and `items` with `id`, `label`, `owner`, `due`, `done`). Ticking a task publishes the ids of all
|
|
18
|
+
* done tasks as the `value` output and runs the behaviors attached to the element. An open task past its due date says "Overdue" in
|
|
19
|
+
* words as well as in color. Progress is a labelled progress bar, so "2 of 5 complete" is read, not just seen. With `editPermission`
|
|
20
|
+
* not enabled the tasks are shown and cannot be changed.
|
|
21
|
+
*/
|
|
22
|
+
export function ChecklistSection({ element, interactions, bindingOutputs }: RegisteredComponentProps) {
|
|
23
|
+
const supplied = useMemo(() => readChecklist(element.properties.sections), [element.properties.sections]);
|
|
24
|
+
const suppliedKey = JSON.stringify(supplied);
|
|
25
|
+
const [groups, setGroups] = useState<ChecklistGroup[]>(supplied);
|
|
26
|
+
const strings = readFoundationStrings(element);
|
|
27
|
+
const locale = readLocale(element);
|
|
28
|
+
const state = readComponentState(element, supplied.some(group => group.items.length > 0));
|
|
29
|
+
const locked = readPermissionRule(element, 'editPermission') !== 'enabled';
|
|
30
|
+
const now = useMemo(() => (typeof element.properties.today === 'string' ? new Date(element.properties.today) : new Date()), [element.properties.today]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => setGroups(supplied), [suppliedKey]);
|
|
33
|
+
useValueOutput(bindingOutputs, doneTaskIds(groups));
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<FoundationFrame element={element} interactions={interactions} name='checklist' state={state} skeleton={[40, 100, 90, 95]}>
|
|
37
|
+
{(state === ComponentState.Ready || state === ComponentState.Partial) && groups.map((group, groupIndex) => {
|
|
38
|
+
const done = completed(group);
|
|
39
|
+
const total = group.items.length;
|
|
40
|
+
return (
|
|
41
|
+
<section key={group.title + groupIndex} className='foundation-checklist-group' aria-labelledby={`${element.id}-group-${groupIndex}`}>
|
|
42
|
+
<header className='foundation-checklist-header'>
|
|
43
|
+
<h3 id={`${element.id}-group-${groupIndex}`}>{group.title}</h3>
|
|
44
|
+
<span className='foundation-count'>{formatText(strings.checklistProgress, { done, total })}</span>
|
|
45
|
+
</header>
|
|
46
|
+
<div className='foundation-progress' role='progressbar' aria-label={formatText(strings.checklistItemProgress, { title: group.title })}
|
|
47
|
+
aria-valuemin={0} aria-valuemax={total} aria-valuenow={done} aria-valuetext={formatText(strings.checklistProgress, { done, total })}>
|
|
48
|
+
<span className='foundation-progress-bar' style={{ inlineSize: total === 0 ? '0%' : `${(done / total) * 100}%` }} />
|
|
49
|
+
</div>
|
|
50
|
+
{total === 0 ? <p className='foundation-count'>{strings.checklistNone}</p> : (
|
|
51
|
+
<ul className='foundation-checklist-items'>
|
|
52
|
+
{group.items.map(item => {
|
|
53
|
+
const overdue = isOverdue(item, now);
|
|
54
|
+
return (
|
|
55
|
+
<li key={item.id} className='foundation-checklist-item' data-done={item.done} data-overdue={overdue}>
|
|
56
|
+
<label className='foundation-check'>
|
|
57
|
+
<input type='checkbox' checked={item.done} disabled={locked} onChange={() => { setGroups(toggleTask(groups, item.id)); interactions?.onChange?.(); }} />
|
|
58
|
+
<span className='foundation-checklist-label'>{item.label}</span>
|
|
59
|
+
</label>
|
|
60
|
+
<span className='foundation-checklist-meta'>
|
|
61
|
+
{item.owner && <span><span className='foundation-visually-hidden'>{`${strings.checklistOwner}: `}</span><FoundationIcon name='user' /> {item.owner}</span>}
|
|
62
|
+
{item.due && (
|
|
63
|
+
<span>
|
|
64
|
+
<span className='foundation-visually-hidden'>{`${strings.checklistDue}: `}</span>
|
|
65
|
+
<time dateTime={item.due}>{formatDate(item.due, locale)}</time>
|
|
66
|
+
{overdue && <strong className='foundation-overdue'> · <FoundationIcon name='alert' /> {strings.checklistOverdue}</strong>}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</span>
|
|
70
|
+
</li>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</ul>
|
|
74
|
+
)}
|
|
75
|
+
</section>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</FoundationFrame>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ChangeEvent, DragEvent, useRef, useState } from 'react';
|
|
5
|
+
import { RegisteredComponentProps, useValueOutput } from '@cratis/scene.react';
|
|
6
|
+
import { readOptionalString } from '../shell/elementProperties';
|
|
7
|
+
import { ActionButton } from './ActionButton';
|
|
8
|
+
import { ComponentState, readComponentState } from './ComponentState';
|
|
9
|
+
import { useFileUploaderHandler } from './FileUploaderHandler';
|
|
10
|
+
import { FoundationIcon } from './FoundationIcon';
|
|
11
|
+
import { readLocale } from './formatting';
|
|
12
|
+
import { formatText, readFoundationStrings } from './foundationStrings';
|
|
13
|
+
import { readPermissionRule } from './PermissionRule';
|
|
14
|
+
import { FoundationFrame } from './StateFrame';
|
|
15
|
+
import { UploadRefusal, UploadStatus, checkSelection, formatBytes } from './uploadModel';
|
|
16
|
+
|
|
17
|
+
interface Entry {
|
|
18
|
+
file: File;
|
|
19
|
+
status: UploadStatus;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The `fileUploader` component: a drop zone and a button to choose files, checked against type and size before anything is sent.
|
|
24
|
+
*
|
|
25
|
+
* `accept` is an `accept` list, `maxSizeMb` the largest file, and `multiple` whether more than one is allowed. The zone is a real file
|
|
26
|
+
* input behind a labelled button, so it works from the keyboard and with a screen reader; dropping onto it does the same as choosing.
|
|
27
|
+
* Refusals are announced with their reason. Accepted files are listed with a remove button and a status in words; the selection
|
|
28
|
+
* (name, size, type) is published as the `value` output. With a `FileUploaderHandlerProvider` in scope the files are sent through it and
|
|
29
|
+
* marked uploading, done or failed; without one the component only collects. `actionPermission` gates choosing files.
|
|
30
|
+
*/
|
|
31
|
+
export function FileUploader({ element, interactions, bindingOutputs }: RegisteredComponentProps) {
|
|
32
|
+
const strings = readFoundationStrings(element);
|
|
33
|
+
const locale = readLocale(element);
|
|
34
|
+
const handler = useFileUploaderHandler();
|
|
35
|
+
const [entries, setEntries] = useState<Entry[]>([]);
|
|
36
|
+
const [problems, setProblems] = useState<string[]>([]);
|
|
37
|
+
const [dragging, setDragging] = useState(false);
|
|
38
|
+
const input = useRef<HTMLInputElement>(null);
|
|
39
|
+
const accept = readOptionalString(element, 'accept') ?? '';
|
|
40
|
+
const maximumBytes = (typeof element.properties.maxSizeMb === 'number' ? element.properties.maxSizeMb : 0) * 1024 * 1024;
|
|
41
|
+
const multiple = element.properties.multiple !== false;
|
|
42
|
+
const rule = readPermissionRule(element, 'actionPermission');
|
|
43
|
+
const state = readComponentState(element, true);
|
|
44
|
+
const serverUrl = readOptionalString(element, 'serverUrl');
|
|
45
|
+
|
|
46
|
+
useValueOutput(bindingOutputs, entries.map(entry => ({ name: entry.file.name, size: entry.file.size, type: entry.file.type, status: entry.status })));
|
|
47
|
+
|
|
48
|
+
const send = async (files: File[]) => {
|
|
49
|
+
if (!handler) return;
|
|
50
|
+
const mark = (status: UploadStatus) => setEntries(current => current.map(entry => (files.includes(entry.file) ? { ...entry, status } : entry)));
|
|
51
|
+
mark(UploadStatus.Uploading);
|
|
52
|
+
try {
|
|
53
|
+
await handler(files, serverUrl);
|
|
54
|
+
mark(UploadStatus.Done);
|
|
55
|
+
} catch {
|
|
56
|
+
mark(UploadStatus.Failed);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const take = (chosen: File[]) => {
|
|
61
|
+
const { accepted, refused } = checkSelection(chosen, { accept, maximumBytes, multiple }, entries.length);
|
|
62
|
+
setProblems(refused.map(({ file, reason }) => reason === UploadRefusal.WrongType ? formatText(strings.uploadWrongType, { name: file.name })
|
|
63
|
+
: reason === UploadRefusal.TooLarge ? formatText(strings.uploadTooLarge, { name: file.name, limit: formatBytes(maximumBytes, locale) }) : strings.uploadSingle));
|
|
64
|
+
if (accepted.length === 0) return;
|
|
65
|
+
|
|
66
|
+
setEntries(current => [...current, ...accepted.map(file => ({ file, status: UploadStatus.Queued }))]);
|
|
67
|
+
interactions?.onChange?.();
|
|
68
|
+
void send(accepted);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const choose = (event: ChangeEvent<HTMLInputElement>) => {
|
|
72
|
+
take(Array.from(event.target.files ?? []));
|
|
73
|
+
event.target.value = '';
|
|
74
|
+
};
|
|
75
|
+
const drop = (event: DragEvent) => {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
setDragging(false);
|
|
78
|
+
if (rule === 'enabled') take(Array.from(event.dataTransfer.files));
|
|
79
|
+
};
|
|
80
|
+
const statusText: Record<UploadStatus, string> = {
|
|
81
|
+
queued: strings.uploadQueued, uploading: strings.uploadUploading, done: strings.uploadDone, failed: strings.uploadFailed,
|
|
82
|
+
};
|
|
83
|
+
const limits = [accept ? formatText(strings.uploadAccepts, { types: accept }) : '', maximumBytes > 0 ? formatText(strings.uploadLimits, { limit: formatBytes(maximumBytes, locale) }) : ''].filter(Boolean).join(' · ');
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<FoundationFrame element={element} interactions={interactions} name='file-uploader' state={state} skeleton={[100, 40]}>
|
|
87
|
+
{state !== ComponentState.Loading && (
|
|
88
|
+
<>
|
|
89
|
+
<div className='foundation-dropzone' data-dragging={dragging} onDragOver={event => { event.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)} onDrop={drop}>
|
|
90
|
+
<FoundationIcon name='upload' />
|
|
91
|
+
<span>{strings.uploadDrop}</span>
|
|
92
|
+
<input ref={input} className='foundation-visually-hidden' type='file' tabIndex={-1} accept={accept || undefined} multiple={multiple} disabled={rule !== 'enabled'} onChange={choose} aria-hidden='true' />
|
|
93
|
+
<ActionButton rule={rule} emphasis='secondary' disabledReason={strings.disabledNoPermission} onClick={() => input.current?.click()}>
|
|
94
|
+
{strings.uploadBrowse}
|
|
95
|
+
</ActionButton>
|
|
96
|
+
{limits && <span className='foundation-count'>{limits}</span>}
|
|
97
|
+
</div>
|
|
98
|
+
<div aria-live='polite' className='foundation-upload-problems'>
|
|
99
|
+
{problems.map(problem => <p key={problem} className='foundation-message-text' data-tone='danger'>{problem}</p>)}
|
|
100
|
+
</div>
|
|
101
|
+
{entries.length > 0 && (
|
|
102
|
+
<ul className='foundation-upload-list' aria-label={strings.uploadSelected}>
|
|
103
|
+
{entries.map((entry, index) => (
|
|
104
|
+
<li key={entry.file.name + index} className='foundation-upload-item' data-status={entry.status}>
|
|
105
|
+
<FoundationIcon name='file' />
|
|
106
|
+
<span className='foundation-upload-name'>{entry.file.name}</span>
|
|
107
|
+
<span className='foundation-count'>{formatBytes(entry.file.size, locale)}</span>
|
|
108
|
+
<span className='foundation-upload-status'>{statusText[entry.status]}</span>
|
|
109
|
+
{entry.status === UploadStatus.Failed && <button type='button' className='foundation-link' onClick={() => void send([entry.file])}>{strings.retry}</button>}
|
|
110
|
+
<button type='button' className='foundation-icon-button' aria-label={formatText(strings.uploadRemove, { name: entry.file.name })}
|
|
111
|
+
onClick={() => { setEntries(current => current.filter(existing => existing !== entry)); interactions?.onChange?.(); }}>
|
|
112
|
+
<FoundationIcon name='close' />
|
|
113
|
+
</button>
|
|
114
|
+
</li>
|
|
115
|
+
))}
|
|
116
|
+
</ul>
|
|
117
|
+
)}
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</FoundationFrame>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ReactNode, createContext, useContext } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What sends the accepted files somewhere: a host's server client, kept out of the portable package.
|
|
8
|
+
*
|
|
9
|
+
* The signature is the one `@cratis/scene.primereact` uses for its `fileUpload`, so a host hands the same function to both. A handler that
|
|
10
|
+
* rejects marks the files as failed, with a retry; without a handler the uploader only collects and publishes the selection.
|
|
11
|
+
*/
|
|
12
|
+
export type FileUploaderHandler = (files: File[], serverUrl?: string) => Promise<void>;
|
|
13
|
+
|
|
14
|
+
const Context = createContext<FileUploaderHandler | undefined>(undefined);
|
|
15
|
+
|
|
16
|
+
/** Provides the upload handler to the file uploaders below it. */
|
|
17
|
+
export function FileUploaderHandlerProvider({ handler, children }: { handler?: FileUploaderHandler; children: ReactNode }) {
|
|
18
|
+
return <Context.Provider value={handler}>{children}</Context.Provider>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The handler in scope, if any. */
|
|
22
|
+
export const useFileUploaderHandler = (): FileUploaderHandler | undefined => useContext(Context);
|
|
@@ -44,3 +44,11 @@ export const ColumnChooserLight: Story = { args: { component: ComponentName.Colu
|
|
|
44
44
|
export const ColumnChooserDark: Story = { args: { component: ComponentName.ColumnChooser, theme: 'dark' } };
|
|
45
45
|
export const SavedViewsMenuLight: Story = { args: { component: ComponentName.SavedViewsMenu, theme: 'light' } };
|
|
46
46
|
export const SavedViewsMenuDark: Story = { args: { component: ComponentName.SavedViewsMenu, theme: 'dark' } };
|
|
47
|
+
export const ChecklistLight: Story = { args: { component: ComponentName.ChecklistSection, theme: 'light' } };
|
|
48
|
+
export const ChecklistDark: Story = { args: { component: ComponentName.ChecklistSection, theme: 'dark' } };
|
|
49
|
+
export const ActivityFeedLight: Story = { args: { component: ComponentName.ActivityFeed, theme: 'light' } };
|
|
50
|
+
export const ActivityFeedDark: Story = { args: { component: ComponentName.ActivityFeed, theme: 'dark' } };
|
|
51
|
+
export const AvatarStackLight: Story = { args: { component: ComponentName.AvatarStack, theme: 'light' } };
|
|
52
|
+
export const AvatarStackDark: Story = { args: { component: ComponentName.AvatarStack, theme: 'dark' } };
|
|
53
|
+
export const FileUploaderLight: Story = { args: { component: ComponentName.FileUploader, theme: 'light' } };
|
|
54
|
+
export const FileUploaderDark: Story = { args: { component: ComponentName.FileUploader, theme: 'dark' } };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/** What kind of thing happened, which decides the icon and the word that tells it apart without color. */
|
|
5
|
+
export enum ActivityKind {
|
|
6
|
+
Comment = 'comment',
|
|
7
|
+
Change = 'change',
|
|
8
|
+
Status = 'status',
|
|
9
|
+
System = 'system',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** One thing that happened to a record. */
|
|
13
|
+
export interface ActivityEntry {
|
|
14
|
+
id: string;
|
|
15
|
+
actor: string;
|
|
16
|
+
action: string;
|
|
17
|
+
kind: ActivityKind;
|
|
18
|
+
at: string;
|
|
19
|
+
body?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The entries of one day, in the order the feed shows them. */
|
|
23
|
+
export interface ActivityDay {
|
|
24
|
+
day: string;
|
|
25
|
+
entries: ActivityEntry[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Reads entries out of a property value, dropping those without an id or a time that parses. */
|
|
29
|
+
export function readActivity(value: unknown): ActivityEntry[] {
|
|
30
|
+
if (!Array.isArray(value)) return [];
|
|
31
|
+
|
|
32
|
+
return value.flatMap((entry): ActivityEntry[] => {
|
|
33
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
|
|
34
|
+
const { id, actor, action, kind, at, body } = entry as Record<string, unknown>;
|
|
35
|
+
if (typeof id !== 'string' || typeof at !== 'string' || Number.isNaN(new Date(at).getTime())) return [];
|
|
36
|
+
|
|
37
|
+
return [{
|
|
38
|
+
id, at,
|
|
39
|
+
actor: typeof actor === 'string' ? actor : '',
|
|
40
|
+
action: typeof action === 'string' ? action : '',
|
|
41
|
+
kind: (Object.values(ActivityKind) as string[]).includes(kind as string) ? (kind as ActivityKind) : ActivityKind.System,
|
|
42
|
+
...(typeof body === 'string' && body.length > 0 ? { body } : {}),
|
|
43
|
+
}];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The calendar day of an instant in a time zone, as `YYYY-MM-DD`. */
|
|
48
|
+
export function dayOf(at: string, timeZone?: string): string {
|
|
49
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Sorts the entries by time (newest first unless `ascending`) and groups them by calendar day, so a long feed has headings to scan by. */
|
|
53
|
+
export function groupByDay(entries: ActivityEntry[], ascending = false, timeZone?: string): ActivityDay[] {
|
|
54
|
+
const sorted = [...entries].sort((first, second) => (new Date(first.at).getTime() - new Date(second.at).getTime()) * (ascending ? 1 : -1));
|
|
55
|
+
const days: ActivityDay[] = [];
|
|
56
|
+
|
|
57
|
+
for (const entry of sorted) {
|
|
58
|
+
const day = dayOf(entry.at, timeZone);
|
|
59
|
+
const last = days[days.length - 1];
|
|
60
|
+
if (last?.day === day) last.entries.push(entry);
|
|
61
|
+
else days.push({ day, entries: [entry] });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return days;
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/** A person shown as an avatar. */
|
|
5
|
+
export interface AvatarPerson {
|
|
6
|
+
name: string;
|
|
7
|
+
imageUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Reads people out of a property value, dropping entries without a name. */
|
|
11
|
+
export function readPeople(value: unknown): AvatarPerson[] {
|
|
12
|
+
if (!Array.isArray(value)) return [];
|
|
13
|
+
|
|
14
|
+
return value.flatMap((entry): AvatarPerson[] => {
|
|
15
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
|
|
16
|
+
const { name, imageUrl } = entry as Record<string, unknown>;
|
|
17
|
+
if (typeof name !== 'string' || name.trim().length === 0) return [];
|
|
18
|
+
return [{ name: name.trim(), ...(typeof imageUrl === 'string' && imageUrl.length > 0 ? { imageUrl } : {}) }];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The initials of a name: the first letter of the first and last words, so "Ada King Lovelace" is AL and "Ada" is A. */
|
|
23
|
+
export function initialsOf(name: string): string {
|
|
24
|
+
const words = name.split(/\s+/).filter(word => word.length > 0);
|
|
25
|
+
if (words.length === 0) return '';
|
|
26
|
+
|
|
27
|
+
const first = Array.from(words[0])[0] ?? '';
|
|
28
|
+
const last = words.length > 1 ? Array.from(words[words.length - 1])[0] ?? '' : '';
|
|
29
|
+
return `${first}${last}`.toLocaleUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Splits people into those shown and how many are folded into the "+N" chip; at least one is always shown when there are any. */
|
|
33
|
+
export function visiblePeople(people: AvatarPerson[], maximum: number): { shown: AvatarPerson[]; hidden: number } {
|
|
34
|
+
const limit = Math.max(1, Math.floor(maximum));
|
|
35
|
+
return people.length <= limit ? { shown: people, hidden: 0 } : { shown: people.slice(0, limit), hidden: people.length - limit };
|
|
36
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/** One task of a checklist. */
|
|
5
|
+
export interface ChecklistItem {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
owner?: string;
|
|
9
|
+
due?: string;
|
|
10
|
+
done: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A titled group of tasks. */
|
|
14
|
+
export interface ChecklistGroup {
|
|
15
|
+
title: string;
|
|
16
|
+
items: ChecklistItem[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Reads checklist groups out of a property value; a task needs an id and a label, and is open unless it says it is done. */
|
|
20
|
+
export function readChecklist(value: unknown): ChecklistGroup[] {
|
|
21
|
+
if (!Array.isArray(value)) return [];
|
|
22
|
+
|
|
23
|
+
return value.flatMap((entry): ChecklistGroup[] => {
|
|
24
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
|
|
25
|
+
const { title, items } = entry as Record<string, unknown>;
|
|
26
|
+
if (typeof title !== 'string' || !Array.isArray(items)) return [];
|
|
27
|
+
|
|
28
|
+
return [{
|
|
29
|
+
title,
|
|
30
|
+
items: items.flatMap((item): ChecklistItem[] => {
|
|
31
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) return [];
|
|
32
|
+
const { id, label, owner, due, done } = item as Record<string, unknown>;
|
|
33
|
+
if (typeof id !== 'string' || typeof label !== 'string') return [];
|
|
34
|
+
return [{ id, label, done: done === true, ...(typeof owner === 'string' ? { owner } : {}), ...(typeof due === 'string' ? { due } : {}) }];
|
|
35
|
+
}),
|
|
36
|
+
}];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** How many of a group's tasks are done. */
|
|
41
|
+
export const completed = (group: ChecklistGroup): number => group.items.filter(item => item.done).length;
|
|
42
|
+
|
|
43
|
+
/** Whether an open task is past its due date, today counting as not yet late. */
|
|
44
|
+
export function isOverdue(item: ChecklistItem, now: Date): boolean {
|
|
45
|
+
if (item.done || item.due === undefined) return false;
|
|
46
|
+
|
|
47
|
+
const due = new Date(item.due);
|
|
48
|
+
if (Number.isNaN(due.getTime())) return false;
|
|
49
|
+
|
|
50
|
+
const endOfDue = new Date(due);
|
|
51
|
+
endOfDue.setUTCHours(23, 59, 59, 999);
|
|
52
|
+
return endOfDue.getTime() < now.getTime();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Marks one task done or open, leaving every other as it was. */
|
|
56
|
+
export const toggleTask = (groups: ChecklistGroup[], id: string): ChecklistGroup[] =>
|
|
57
|
+
groups.map(group => (group.items.some(item => item.id === id)
|
|
58
|
+
? { ...group, items: group.items.map(item => (item.id === id ? { ...item, done: !item.done } : item)) }
|
|
59
|
+
: group));
|
|
60
|
+
|
|
61
|
+
/** The ids of every done task: what the checklist publishes. */
|
|
62
|
+
export const doneTaskIds = (groups: ChecklistGroup[]): string[] => groups.flatMap(group => group.items.filter(item => item.done).map(item => item.id));
|
package/foundation/formatting.ts
CHANGED
|
@@ -67,3 +67,12 @@ export function formatDate(value: unknown, locale: string, withTime = false): st
|
|
|
67
67
|
return date.toISOString();
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/** Formats a calendar day (`YYYY-MM-DD`) for a locale, as that day and never shifted by the reader's time zone. */
|
|
72
|
+
export function formatDay(day: string, locale: string): string {
|
|
73
|
+
try {
|
|
74
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeZone: 'UTC' }).format(new Date(`${day}T12:00:00Z`));
|
|
75
|
+
} catch {
|
|
76
|
+
return day;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -18,4 +18,8 @@ export const foundationComponentMetadata: Record<string, ComponentMetadata> = {
|
|
|
18
18
|
[ComponentName.FilterBuilder]: m('Filter builder', ComponentCategory.Input, 'list-filter', ['filter', 'query', 'conditions', 'and or', 'search']),
|
|
19
19
|
[ComponentName.ColumnChooser]: m('Column chooser', ComponentCategory.Actions, 'columns-3', ['columns', 'fields', 'show hide', 'table']),
|
|
20
20
|
[ComponentName.SavedViewsMenu]: m('Saved views menu', ComponentCategory.Actions, 'bookmark', ['views', 'presets', 'saved filters']),
|
|
21
|
+
[ComponentName.ChecklistSection]: m('Checklist', ComponentCategory.DataDisplay, 'list-checks', ['tasks', 'todo', 'onboarding', 'progress', 'steps']),
|
|
22
|
+
[ComponentName.ActivityFeed]: m('Activity feed', ComponentCategory.DataDisplay, 'activity', ['timeline', 'history', 'comments', 'audit', 'log']),
|
|
23
|
+
[ComponentName.AvatarStack]: m('Avatar stack', ComponentCategory.DataDisplay, 'users', ['people', 'members', 'assignees', 'team']),
|
|
24
|
+
[ComponentName.FileUploader]: m('File uploader', ComponentCategory.Input, 'upload', ['attachments', 'files', 'drop zone', 'documents']),
|
|
21
25
|
};
|
|
@@ -5,6 +5,7 @@ import { BindingSourceKind, ComponentDescriptor, PropertyDescriptor, PropertyEdi
|
|
|
5
5
|
import { componentRegistryKey, valueOutputProperty } from '@cratis/scene.react';
|
|
6
6
|
import { ComponentName } from '../ComponentName';
|
|
7
7
|
import { defaultBlueprintName } from '../packageName';
|
|
8
|
+
import { ActivityKind } from './activityModel';
|
|
8
9
|
import { ChartKind } from './chartGeometry';
|
|
9
10
|
import { ComponentState } from './ComponentState';
|
|
10
11
|
import { FilterFieldType } from './filterModel';
|
|
@@ -200,5 +201,83 @@ export const foundationDescriptors: ComponentDescriptor[] = [
|
|
|
200
201
|
...standardStateProperties,
|
|
201
202
|
],
|
|
202
203
|
},
|
|
204
|
+
{
|
|
205
|
+
component: key(ComponentName.ChecklistSection),
|
|
206
|
+
displayName: 'Checklist',
|
|
207
|
+
description: 'Sections of tasks with an owner and due date each and a progress bar per section. Publishes the ids of the done tasks as the value output.',
|
|
208
|
+
properties: [
|
|
209
|
+
{
|
|
210
|
+
...list('sections', 'Sections', 'Data', 'Section', [
|
|
211
|
+
text('title', 'Title', 'Content', undefined, { constraints: { required: true } }),
|
|
212
|
+
list('items', 'Tasks', 'Data', 'Task', [
|
|
213
|
+
text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
|
|
214
|
+
text('label', 'Task', 'Content', undefined, { constraints: { required: true } }),
|
|
215
|
+
text('owner', 'Owner', 'Data'),
|
|
216
|
+
text('due', 'Due date', 'Data', 'An ISO 8601 date.'),
|
|
217
|
+
flag('done', 'Done', 'Data'),
|
|
218
|
+
]),
|
|
219
|
+
], 'The groups of tasks.'),
|
|
220
|
+
acceptedBindingKinds: bindable,
|
|
221
|
+
},
|
|
222
|
+
text('today', 'Today', 'Content', 'The date overdue is judged against; empty is the current date.'),
|
|
223
|
+
rule('editPermission', 'Edit permission', 'Enabled lets the user tick tasks; otherwise they are shown read-only.'),
|
|
224
|
+
valueOutputProperty(PropertyValueType.Json, 'The ids of the done tasks, published as the user ticks them.'),
|
|
225
|
+
...standardStateProperties,
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
component: key(ComponentName.ActivityFeed),
|
|
230
|
+
displayName: 'Activity feed',
|
|
231
|
+
description: 'What happened to a record, grouped by day. Content is supplied by the application; this lays it out.',
|
|
232
|
+
properties: [
|
|
233
|
+
{
|
|
234
|
+
...list('entries', 'Entries', 'Data', 'Entry', [
|
|
235
|
+
text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
|
|
236
|
+
text('actor', 'Who', 'Data'),
|
|
237
|
+
text('action', 'What', 'Data'),
|
|
238
|
+
choice('kind', 'Kind', 'Data', {
|
|
239
|
+
[ActivityKind.Comment]: 'Comment', [ActivityKind.Change]: 'Change', [ActivityKind.Status]: 'Status', [ActivityKind.System]: 'System',
|
|
240
|
+
}, ActivityKind.System),
|
|
241
|
+
text('at', 'When', 'Data', 'An ISO 8601 date and time.', { constraints: { required: true } }),
|
|
242
|
+
text('body', 'Text', 'Data', 'A comment\'s text or the detail of a change.', { editorOptions: { multiline: true } }),
|
|
243
|
+
], 'What happened.'),
|
|
244
|
+
acceptedBindingKinds: bindable,
|
|
245
|
+
},
|
|
246
|
+
choice('order', 'Order', 'Appearance', { descending: 'Newest first', ascending: 'Oldest first' }, 'descending'),
|
|
247
|
+
text('timeZone', 'Time zone', 'Content', 'The IANA zone days are cut in; empty is the reader\'s.'),
|
|
248
|
+
...standardStateProperties,
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
component: key(ComponentName.AvatarStack),
|
|
253
|
+
displayName: 'Avatar stack',
|
|
254
|
+
description: 'Overlapping avatars for the people on a record, with a "+N" chip for the rest.',
|
|
255
|
+
properties: [
|
|
256
|
+
{
|
|
257
|
+
...list('people', 'People', 'Data', 'Person', [
|
|
258
|
+
text('name', 'Name', 'Data', undefined, { constraints: { required: true } }),
|
|
259
|
+
text('imageUrl', 'Picture', 'Data', 'Without one the initials are shown.'),
|
|
260
|
+
], 'Who is shown.'),
|
|
261
|
+
acceptedBindingKinds: bindable,
|
|
262
|
+
},
|
|
263
|
+
number('max', 'Shown at most', 'Appearance', 'How many avatars before the rest fold into "+N".', { default: 4, constraints: { minimum: 1, integer: true } }),
|
|
264
|
+
choice('size', 'Size', 'Appearance', { small: 'Small', medium: 'Medium' }, 'medium'),
|
|
265
|
+
...standardStateProperties,
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
component: key(ComponentName.FileUploader),
|
|
270
|
+
displayName: 'File uploader',
|
|
271
|
+
description: 'A drop zone and a button to choose files, checked against type and size before anything is sent. Publishes the selection as the value output.',
|
|
272
|
+
properties: [
|
|
273
|
+
text('accept', 'Accepted types', 'Behavior', 'Extensions, MIME types and wildcards separated by commas, for example ".pdf, image/*".'),
|
|
274
|
+
number('maxSizeMb', 'Largest file (MB)', 'Behavior', undefined, { constraints: { minimum: 0 } }),
|
|
275
|
+
flag('multiple', 'Several files', 'Behavior', undefined, { default: true }),
|
|
276
|
+
text('serverUrl', 'Upload address', 'Behavior', 'Handed to the host\'s upload handler.'),
|
|
277
|
+
rule('actionPermission', 'Upload permission', 'Whether files can be chosen or dropped, refused with a reason, or the control is hidden.'),
|
|
278
|
+
valueOutputProperty(PropertyValueType.Json, 'The files chosen - name, size, type and status - published as they change.'),
|
|
279
|
+
...standardStateProperties.filter(property => property.path !== 'actionPermission'),
|
|
280
|
+
],
|
|
281
|
+
},
|
|
203
282
|
];
|
|
204
283
|
|